From 0f6fbc425b535677b93e1198c523a4a7696ece84 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 11:56:21 -0200 Subject: [PATCH 01/45] ADDED: Pre-flight checklist instructions --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 680d35d2..6eff0739 100644 --- a/README.rst +++ b/README.rst @@ -29,6 +29,7 @@ Dependencies (pre-flight checklist) 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. +3.1 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 From a49e83fbd08df9030b1f894ade2adea74d578d22 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 11:57:35 -0200 Subject: [PATCH 02/45] UDPATED: RST formatting. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 6eff0739..5b4c3bd3 100644 --- a/README.rst +++ b/README.rst @@ -28,8 +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. -3.1 Also, make sure that both "django.contrib.humanize" and "django.contrib.sites" are installed. + 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 From 4d00dd3d6ec9a07bb613bdcf1197a7f215fc1bae Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 7 Feb 2016 12:06:07 -0200 Subject: [PATCH 03/45] 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 04/45] 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 05/45] 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 06/45] 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 07/45] 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 08/45] 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 09/45] 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 10/45] 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 11/45] 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 12/45] 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 13/45] 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 14/45] 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 15/45] 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 16/45] 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 17/45] 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 18/45] 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 19/45] 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 20/45] 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 21/45] 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 22/45] 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 23/45] 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 24/45] 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 25/45] 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 26/45] 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 27/45] 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 28/45] 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 29/45] 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 30/45] 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 31/45] 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 32/45] 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)) From 56b7f53c51bd7f7132feb29e490df37f884578af Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sat, 20 Feb 2016 17:46:03 -0200 Subject: [PATCH 33/45] CLEANUP: Removing necessary code from feature 2 --- helpdesk/models.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/helpdesk/models.py b/helpdesk/models.py index 12bb8b74..42d7ef2b 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -428,24 +428,6 @@ 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_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) - - 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 From 19f04aa1f5a1f0b0674b48a7003302ea4cc6b630 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sat, 20 Feb 2016 17:49:37 -0200 Subject: [PATCH 34/45] CLEANUP: Removing necessary code from feature 2 --- helpdesk/management/commands/get_email.py | 1 - 1 file changed, 1 deletion(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 2a47139e..14faeedd 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -367,7 +367,6 @@ def object_from_message(message, queue, quiet): msg = message - #import ipdb;ipdb.set_trace() message = email.message_from_string(msg) subject = message.get('subject', _('Created from e-mail')) From 0c480b4abd5987b367a3ce74edc6ee2f56d68c47 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Mon, 22 Feb 2016 10:00:29 -0300 Subject: [PATCH 35/45] UPDATE: Removed an unnecessary migration created by mistake. --- ..._add_submitter_email_id_field_to_ticket.py | 6 ++--- ...re_message_id_field_into_followup_model.py | 24 ------------------- 2 files changed, 3 insertions(+), 27 deletions(-) delete mode 100644 helpdesk/migrations/0013_store_message_id_field_into_followup_model.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 index beafe939..1a932260 100644 --- a/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py +++ b/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py @@ -13,8 +13,8 @@ class Migration(migrations.Migration): 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'), + model_name='followup', + name='message_id', + field=models.CharField(blank=True, editable=False, help_text="The Message ID of the submitter's email.", max_length=256, null=True, verbose_name='E-Mail ID'), ), ] diff --git a/helpdesk/migrations/0013_store_message_id_field_into_followup_model.py b/helpdesk/migrations/0013_store_message_id_field_into_followup_model.py deleted file mode 100644 index 2c1106c7..00000000 --- a/helpdesk/migrations/0013_store_message_id_field_into_followup_model.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- 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'), - ), - ] From b55eaabebdfd197a8a9cc4061c987d177ba0acfa Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Mon, 22 Feb 2016 10:17:28 -0300 Subject: [PATCH 36/45] UPDATED: Renaming methods after reading through the pull request --- helpdesk/management/commands/get_email.py | 10 +++++----- helpdesk/tests/test_ticket_submission.py | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 14faeedd..9ae72546 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -124,9 +124,9 @@ def process_queue(q, quiet=False): msgSize = msg.split(" ")[1] full_message = "\n".join(server.retr(msgNum)[1]) - ticket = object_from_message(message=full_message, queue=q, quiet=quiet) + obj = parse_mail_message(message=full_message, queue=q, quiet=quiet) - if ticket: + if obj: server.dele(msgNum) server.quit() @@ -147,8 +147,8 @@ def process_queue(q, quiet=False): msgnums = data[0].split() for num in msgnums: status, data = server.fetch(num, '(RFC822)') - ticket = object_from_message(message=data[0][1], queue=q, quiet=quiet) - if ticket: + obj = parse_mail_message(message=data[0][1], queue=q, quiet=quiet) + if obj: server.store(num, '+FLAGS', '\\Deleted') server.expunge() @@ -362,7 +362,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): return ticket -def object_from_message(message, queue, quiet): +def parse_mail_message(message, queue, quiet): # 'message' must be an RFC822 formatted message. msg = message diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index a63dd266..b1b8cad9 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -10,7 +10,7 @@ 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 +from helpdesk.management.commands.get_email import parse_mail_message, create_ticket_cc try: # python 3 from urllib.parse import urlparse @@ -69,7 +69,7 @@ class TicketBasicsTestCase(TestCase): #for m in mail.outbox: # print m.to, m.subject - object_from_message(str(msg), self.queue_public, quiet=True) + parse_mail_message(str(msg), self.queue_public, quiet=True) followup = FollowUp.objects.get(message_id=message_id) ticket = Ticket.objects.get(id=followup.ticket.id) @@ -99,7 +99,7 @@ class TicketBasicsTestCase(TestCase): email_count = len(mail.outbox) - object_from_message(str(msg), self.queue_public, quiet=True) + parse_mail_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) @@ -132,7 +132,7 @@ class TicketBasicsTestCase(TestCase): email_count = len(mail.outbox) - object_from_message(str(msg), self.queue_public, quiet=True) + parse_mail_message(str(msg), self.queue_public, quiet=True) followup = FollowUp.objects.get(message_id=message_id) ticket = Ticket.objects.get(id=followup.ticket.id) @@ -172,7 +172,7 @@ class TicketBasicsTestCase(TestCase): email_count = len(mail.outbox) - self.assertRaises(ValidationError, object_from_message, str(msg), self.queue_public, quiet=True) + self.assertRaises(ValidationError, parse_mail_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): @@ -197,7 +197,7 @@ class TicketBasicsTestCase(TestCase): email_count = len(mail.outbox) - object_from_message(str(msg), self.queue_public, quiet=True) + parse_mail_message(str(msg), self.queue_public, quiet=True) followup = FollowUp.objects.get(message_id=message_id) ticket = Ticket.objects.get(id=followup.ticket.id) @@ -219,7 +219,7 @@ class TicketBasicsTestCase(TestCase): reply.__setitem__('Content-Type', 'text/plain;') reply.set_payload(self.ticket_data['description']) - object_from_message(str(reply), self.queue_public, quiet=True) + parse_mail_message(str(reply), self.queue_public, quiet=True) followup = FollowUp.objects.get(message_id=message_id) ticket = Ticket.objects.get(id=followup.ticket.id) @@ -269,7 +269,7 @@ class TicketBasicsTestCase(TestCase): email_count = len(mail.outbox) - object_from_message(str(msg), self.queue_public, quiet=True) + parse_mail_message(str(msg), self.queue_public, quiet=True) followup = FollowUp.objects.get(message_id=message_id) ticket = Ticket.objects.get(id=followup.ticket.id) @@ -303,7 +303,7 @@ class TicketBasicsTestCase(TestCase): reply.__setitem__('Content-Type', 'text/plain;') reply.set_payload(self.ticket_data['description']) - object_from_message(str(reply), self.queue_public, quiet=True) + parse_mail_message(str(reply), self.queue_public, quiet=True) followup = FollowUp.objects.get(message_id=message_id) ticket = Ticket.objects.get(id=followup.ticket.id) @@ -353,7 +353,7 @@ class TicketBasicsTestCase(TestCase): email_count = len(mail.outbox) - object_from_message(str(msg), self.queue_public, quiet=True) + parse_mail_message(str(msg), self.queue_public, quiet=True) followup = FollowUp.objects.get(message_id=message_id) ticket = Ticket.objects.get(id=followup.ticket.id) @@ -392,7 +392,7 @@ class TicketBasicsTestCase(TestCase): email_count = len(mail.outbox) - object_from_message(str(reply), self.queue_public, quiet=True) + parse_mail_message(str(reply), self.queue_public, quiet=True) followup = FollowUp.objects.get(message_id=message_id) ticket = Ticket.objects.get(id=followup.ticket.id) From 5058790995933b155d87605b7c66b31b403f5b5c Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Wed, 24 Feb 2016 10:42:14 -0300 Subject: [PATCH 37/45] BUGFIX: Fixing migration merge conflict. --- ...ticket.py => 0013_add_submitter_email_id_field_to_ticket.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename helpdesk/migrations/{0012_add_submitter_email_id_field_to_ticket.py => 0013_add_submitter_email_id_field_to_ticket.py} (90%) diff --git a/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py b/helpdesk/migrations/0013_add_submitter_email_id_field_to_ticket.py similarity index 90% rename from helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py rename to helpdesk/migrations/0013_add_submitter_email_id_field_to_ticket.py index 1a932260..e312c544 100644 --- a/helpdesk/migrations/0012_add_submitter_email_id_field_to_ticket.py +++ b/helpdesk/migrations/0013_add_submitter_email_id_field_to_ticket.py @@ -8,7 +8,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('helpdesk', '0011_admin_related_improvements'), + ('helpdesk', '0012_queue_default_owner'), ] operations = [ From 45fbd605e54a9266b004bc7641c2ac671b41d7e4 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 28 Feb 2016 15:00:15 -0300 Subject: [PATCH 38/45] UPDATED: Separate HTTP and e-mail TestCases --- helpdesk/tests/test_ticket_submission.py | 408 ++++++++++++++++++++++- 1 file changed, 407 insertions(+), 1 deletion(-) diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index b1b8cad9..6ab3dce7 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -411,7 +411,6 @@ class TicketBasicsTestCase(TestCase): # 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) @@ -483,3 +482,410 @@ class TicketBasicsTestCase(TestCase): # Ensure only two e-mails were sent - submitter & updated. self.assertEqual(email_count+2, len(mail.outbox)) + +class MailBasicsTestCase(TicketBasicsTestCase): + fixtures = ['emailtemplate.json'] + + def setUp(self): + self.queue_public = Queue.objects.create(title='Queue 1', slug='q1', allow_public_submission=True, new_ticket_cc='new.public@example.com', updated_ticket_cc='update.public@example.com') + self.queue_private = Queue.objects.create(title='Queue 2', slug='q2', allow_public_submission=False, new_ticket_cc='new.private@example.com', updated_ticket_cc='update.private@example.com') + + self.queue_public_with_notification_enabled = Queue.objects.create(title='Queue 3', + slug='q3', + allow_public_submission=True, + new_ticket_cc='new.public.with.notifications@example.com', + updated_ticket_cc='update.public.with.notifications@example.com' + ) + + self.queue_public_with_notification_disabled = Queue.objects.create(title='Queue 4', + slug='q4', + allow_public_submission=True, + new_ticket_cc='new.public.without.notifications@example.com', + updated_ticket_cc='update.public.without.notifications@example.com' + ) + + self.ticket_data = { + 'title': 'Test Ticket', + 'description': 'Some Test Ticket', + } + + self.client = Client() + + def test_create_ticket_instance_from_payload(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. + """ + + 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 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): + + """ + Ensure that an instance of is created for every valid element of the + "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'] + + 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 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 instance is created if an invalid element of the + "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'] + + 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 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) + + 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 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) + + # 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 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) + + # 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 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) + + # 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 + 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) + + # 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 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) + + # 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)) From 1511e0e72c4f7a36805cb42b63edae559aedebb0 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Sun, 28 Feb 2016 15:19:08 -0300 Subject: [PATCH 39/45] UPDATED: Better separation of HTTP and e-mail test cases --- helpdesk/management/commands/get_email.py | 11 +- helpdesk/tests/test_ticket_submission.py | 417 +--------------------- 2 files changed, 23 insertions(+), 405 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 9ae72546..5028c098 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -124,9 +124,9 @@ def process_queue(q, quiet=False): msgSize = msg.split(" ")[1] full_message = "\n".join(server.retr(msgNum)[1]) - obj = parse_mail_message(message=full_message, queue=q, quiet=quiet) + ticket = object_from_message(message=full_message, queue=q, quiet=quiet) - if obj: + if ticket: server.dele(msgNum) server.quit() @@ -147,8 +147,8 @@ def process_queue(q, quiet=False): msgnums = data[0].split() for num in msgnums: status, data = server.fetch(num, '(RFC822)') - obj = parse_mail_message(message=data[0][1], queue=q, quiet=quiet) - if obj: + ticket = object_from_message(message=data[0][1], queue=q, quiet=quiet) + if ticket: server.store(num, '+FLAGS', '\\Deleted') server.expunge() @@ -362,7 +362,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): return ticket -def parse_mail_message(message, queue, quiet): +def object_from_message(message, queue, quiet): # 'message' must be an RFC822 formatted message. msg = message @@ -462,4 +462,3 @@ def parse_mail_message(message, queue, quiet): if __name__ == '__main__': process_email() - diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index 6ab3dce7..b9b389bd 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -10,7 +10,7 @@ from django.forms import ValidationError from django.test.client import Client from django.core.urlresolvers import reverse -from helpdesk.management.commands.get_email import parse_mail_message, create_ticket_cc +from helpdesk.management.commands.get_email import object_from_message, create_ticket_cc try: # python 3 from urllib.parse import urlparse @@ -44,373 +44,6 @@ 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_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() - - 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 - - parse_mail_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 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) - - parse_mail_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 is created for every valid element of the - "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'] - - 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) - - parse_mail_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 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 instance is created if an invalid element of the - "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'] - - 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, parse_mail_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) - - parse_mail_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']) - - parse_mail_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) - - # 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 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) - - parse_mail_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) - - # 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']) - - parse_mail_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) - - # 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 - 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) - - parse_mail_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) - - # 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) - - parse_mail_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) - - # 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) @@ -483,22 +116,26 @@ class TicketBasicsTestCase(TestCase): # Ensure only two e-mails were sent - submitter & updated. self.assertEqual(email_count+2, len(mail.outbox)) -class MailBasicsTestCase(TicketBasicsTestCase): +class EmailInteractionsTestCase(TestCase): fixtures = ['emailtemplate.json'] def setUp(self): - self.queue_public = Queue.objects.create(title='Queue 1', slug='q1', allow_public_submission=True, new_ticket_cc='new.public@example.com', updated_ticket_cc='update.public@example.com') - self.queue_private = Queue.objects.create(title='Queue 2', slug='q2', allow_public_submission=False, new_ticket_cc='new.private@example.com', updated_ticket_cc='update.private@example.com') + self.queue_public = Queue.objects.create(title='Mail Queue 1', + slug='mq1', + allow_public_submission=True, + new_ticket_cc='new.public@example.com', + updated_ticket_cc='update.public@example.com' + ) - self.queue_public_with_notification_enabled = Queue.objects.create(title='Queue 3', - slug='q3', + self.queue_public_with_notification_enabled = Queue.objects.create(title='Mail Queue 2', + slug='mq2', allow_public_submission=True, new_ticket_cc='new.public.with.notifications@example.com', updated_ticket_cc='update.public.with.notifications@example.com' ) - self.queue_public_with_notification_disabled = Queue.objects.create(title='Queue 4', - slug='q4', + self.queue_public_with_notification_disabled = Queue.objects.create(title='Mail Queue 3', + slug='mq3', allow_public_submission=True, new_ticket_cc='new.public.without.notifications@example.com', updated_ticket_cc='update.public.without.notifications@example.com' @@ -509,20 +146,6 @@ class MailBasicsTestCase(TicketBasicsTestCase): 'description': 'Some Test Ticket', } - self.client = Client() - - def test_create_ticket_instance_from_payload(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): """ @@ -544,16 +167,13 @@ class MailBasicsTestCase(TicketBasicsTestCase): 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) + self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id) # As we have created an Ticket from an email, we notify the sender (+1) # and the new and update queues (+2) @@ -582,7 +202,7 @@ class MailBasicsTestCase(TicketBasicsTestCase): 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) + self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id) # As we have created an Ticket from an email, we notify the sender (+1) # and the new and update queues (+2) @@ -615,7 +235,7 @@ class MailBasicsTestCase(TicketBasicsTestCase): 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(ticket.ticket_for_url, "mq1-%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 @@ -702,7 +322,7 @@ class MailBasicsTestCase(TicketBasicsTestCase): 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(ticket.ticket_for_url, "mq1-%s" % ticket.id) # Ensure that is created for cc_email in cc_list: @@ -722,7 +342,6 @@ class MailBasicsTestCase(TicketBasicsTestCase): 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): """ @@ -786,7 +405,7 @@ class MailBasicsTestCase(TicketBasicsTestCase): 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(ticket.ticket_for_url, "mq1-%s" % ticket.id) # Ensure that is created for cc_email in cc_list: @@ -875,7 +494,7 @@ class MailBasicsTestCase(TicketBasicsTestCase): 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(ticket.ticket_for_url, "mq1-%s" % ticket.id) # Ensure that is created for cc_email in cc_list: From 26fc6cb1de849cefe519afc1c08d4b9356503763 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Tue, 1 Mar 2016 16:49:34 -0300 Subject: [PATCH 40/45] ADDED: Flag to enable notifications on every interaction that occurs over email. --- helpdesk/management/commands/get_email.py | 2 +- ...notifications_on_email_events_to_ticket.py | 20 ++++ helpdesk/models.py | 9 ++ helpdesk/tests/test_ticket_submission.py | 99 ++++++++++++++++--- 4 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 helpdesk/migrations/0014_add_enable_notifications_on_email_events_to_ticket.py diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 5028c098..0335dad6 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -348,7 +348,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): for email in ticket_cc_list : notifications_to_be_sent.append(email) - if len(notifications_to_be_sent): + if queue.enable_notifications_on_email_events and len(notifications_to_be_sent): send_templated_mail( notification_template, diff --git a/helpdesk/migrations/0014_add_enable_notifications_on_email_events_to_ticket.py b/helpdesk/migrations/0014_add_enable_notifications_on_email_events_to_ticket.py new file mode 100644 index 00000000..deb1ef04 --- /dev/null +++ b/helpdesk/migrations/0014_add_enable_notifications_on_email_events_to_ticket.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.1 on 2016-03-01 19:43 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('helpdesk', '0013_add_submitter_email_id_field_to_ticket'), + ] + + operations = [ + migrations.AddField( + model_name='queue', + name='enable_notifications_on_email_events', + field=models.BooleanField(default=False, help_text='When an email arrives to either create a ticket or to interact with an existing discussion. Should email notifications be sent ? Note: the new_ticket_cc and updated_ticket_cc work independently of this feature', verbose_name='Notify contacts when email updates arrive'), + ), + ] diff --git a/helpdesk/models.py b/helpdesk/models.py index 1db8a923..374aa1af 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -112,6 +112,15 @@ class Queue(models.Model): 'multiple addresses with a comma.'), ) + enable_notifications_on_email_events = models.BooleanField( + _('Notify contacts when email updates arrive'), + blank=True, + default=False, + help_text=_('When an email arrives to either create a ticket or to ' + 'interact with an existing discussion. Should email notifications be sent ? ' + 'Note: the new_ticket_cc and updated_ticket_cc work independently of this feature'), + ) + email_box_type = models.CharField( _('E-Mail Box Type'), max_length=5, diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index b9b389bd..2ef6cbb1 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -123,22 +123,17 @@ class EmailInteractionsTestCase(TestCase): self.queue_public = Queue.objects.create(title='Mail Queue 1', slug='mq1', allow_public_submission=True, - new_ticket_cc='new.public@example.com', - updated_ticket_cc='update.public@example.com' + new_ticket_cc='new.public.with.notifications@example.com', + updated_ticket_cc='update.public.with.notifications@example.com', + enable_notifications_on_email_events=True, ) - self.queue_public_with_notification_enabled = Queue.objects.create(title='Mail Queue 2', + self.queue_public_with_notifications_disabled = Queue.objects.create(title='Mail Queue 2', slug='mq2', allow_public_submission=True, - new_ticket_cc='new.public.with.notifications@example.com', - updated_ticket_cc='update.public.with.notifications@example.com' - ) - - self.queue_public_with_notification_disabled = Queue.objects.create(title='Mail Queue 3', - slug='mq3', - allow_public_submission=True, new_ticket_cc='new.public.without.notifications@example.com', - updated_ticket_cc='update.public.without.notifications@example.com' + updated_ticket_cc='update.public.without.notifications@example.com', + enable_notifications_on_email_events=False, ) self.ticket_data = { @@ -508,3 +503,85 @@ class EmailInteractionsTestCase(TestCase): # 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_from_email_to_a_notification_enabled_queue(self): + + """ + Ensure that when an email is sent to a Queue with notifications_enabled turned ON, all contacts + on the TicketCC list are notified. + """ + + msg = email.message.Message() + + message_id = uuid.uuid4().hex + submitter_email = 'foo@bar.py' + cc_list = ['bravo@example.net', 'charlie@foobar.com'] + + 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, "mq1-%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 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_to_a_notification_disabled_queue(self): + + """ + Ensure that when an email is sent to a Queue with notifications_enabled turned OFF, only the + new_ticket_cc and updated_ticket_cc contacts (if they are set) are notified. No contact + from the TicketCC list should be notified. + """ + + 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_with_notifications_disabled, quiet=True) + + followup = FollowUp.objects.get(message_id=message_id) + ticket = Ticket.objects.get(id=followup.ticket.id) + self.assertEqual(ticket.ticket_for_url, "mq2-%s" % ticket.id) + + # As we have created an Ticket from an email, we notify the sender (+1), + # the new and update queues (+2) and that's all + self.assertEqual(email_count + 1 + 2, len(mail.outbox)) + + # Ensure that is created even if the Queue notifications are disabled + # so when staff members interact with the , they get notified + for cc_email in cc_list: + ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email) + self.assertTrue(ticket_cc.ticket, ticket) + self.assertTrue(ticket_cc.email, cc_email) + From 4124677f103807c007a360ee6621880ba7128a91 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Thu, 3 Mar 2016 13:47:15 -0300 Subject: [PATCH 41/45] ADDED: Test cases for the new enable_notifications_on_email_events flag --- helpdesk/management/commands/get_email.py | 4 +- helpdesk/tests/test_ticket_submission.py | 212 +++++++++++++++++++++- 2 files changed, 211 insertions(+), 5 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 0335dad6..96cef195 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -345,7 +345,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): notifications_to_be_sent = [] ticket_cc_list = TicketCC.objects.filter(ticket=ticket).all().values_list('email', flat=True) - for email in ticket_cc_list : + for email in ticket_cc_list : notifications_to_be_sent.append(email) if queue.enable_notifications_on_email_events and len(notifications_to_be_sent): @@ -436,8 +436,6 @@ def object_from_message(message, queue, quiet): 'type': 'text/html', }) - - priority = 3 smtp_priority = message.get('priority', '') diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index 2ef6cbb1..3c871f2f 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -507,8 +507,8 @@ class EmailInteractionsTestCase(TestCase): def test_create_ticket_from_email_to_a_notification_enabled_queue(self): """ - Ensure that when an email is sent to a Queue with notifications_enabled turned ON, all contacts - on the TicketCC list are notified. + Ensure that when an email is sent to a Queue with notifications_enabled turned ON, + and a is created, all contacts n the TicketCC list are notified. """ msg = email.message.Message() @@ -585,3 +585,211 @@ class EmailInteractionsTestCase(TestCase): self.assertTrue(ticket_cc.ticket, ticket) self.assertTrue(ticket_cc.email, cc_email) + def test_create_followup_from_email_to_a_notification_enabled_queue(self): + + """ + Ensure that when an email is sent to a Queue with notifications_enabled turned ON, + and a is created, all contacts n the TicketCC list are notified. + """ + + ### 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) + self.assertEqual(ticket.ticket_for_url, "mq1-%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) + expected_email_count = email_count + 1 + 2 + 1 + self.assertEqual(expected_email_count, len(mail.outbox)) + + # 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) + ### end of the Ticket and TicketCCs creation ### + + # Reply message + reply = email.message.Message() + + reply_message_id = uuid.uuid4().hex + submitter_email = 'bravo@example.net' + + reply.__setitem__('Message-ID', reply_message_id) + reply.__setitem__('In-Reply-To', message_id) + reply.__setitem__('Subject', self.ticket_data['title']) + reply.__setitem__('From', submitter_email) + reply.__setitem__('To', self.queue_public.email_address) + reply.__setitem__('Content-Type', 'text/plain;') + reply.set_payload(self.ticket_data['description']) + + 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, "mq1-%s" % ticket.id) + + # As an update was made, we increase the expected_email_count with: + # the ticket submitter: +1 + # public_update_queue: +1 + expected_email_count += 1 + 1 + self.assertEqual(expected_email_count, len(mail.outbox)) + + def test_create_followup_from_email_to_a_notification_diabled_queue(self): + + """ + Ensure that when an email is sent to a Queue with notifications_enabled turned ON, + and a is created, all contacts n the TicketCC list are notified. + """ + + ### 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_with_notifications_disabled.email_address) + msg.__setitem__('Cc', ','.join(cc_list)) + msg.__setitem__('Content-Type', 'text/plain;') + msg.set_payload(self.ticket_data['description']) + + email_count = len(mail.outbox) + + 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, "mq1-%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) + expected_email_count = email_count + 1 + 2 + 1 + self.assertEqual(expected_email_count, len(mail.outbox)) + + + # 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) + ### end of the Ticket and TicketCCs creation ### + + # Reply message + reply = email.message.Message() + + reply_message_id = uuid.uuid4().hex + submitter_email = 'bravo@example.net' + + reply.__setitem__('Message-ID', reply_message_id) + reply.__setitem__('In-Reply-To', message_id) + reply.__setitem__('Subject', self.ticket_data['title']) + reply.__setitem__('From', submitter_email) + reply.__setitem__('To', self.queue_public_with_notifications_disabled.email_address) + reply.__setitem__('Content-Type', 'text/plain;') + reply.set_payload(self.ticket_data['description']) + + object_from_message(str(reply), self.queue_public_with_notifications_disabled, quiet=True) + + followup = FollowUp.objects.get(message_id=message_id) + ticket = Ticket.objects.get(id=followup.ticket.id) + self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id) + + # As an update was made, we increase the expected_email_count with: + # public_update_queue: +1 + expected_email_count += 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 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) + + 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 = 'bravo@example.net' + cc_list = ['foo@bar.py', 'charlie@foobar.com'] + + reply.__setitem__('Message-ID', reply_message_id) + reply.__setitem__('In-Reply-To', message_id) + reply.__setitem__('Subject', self.ticket_data['title']) + reply.__setitem__('From', submitter_email) + reply.__setitem__('To', self.queue_public.email_address) + reply.__setitem__('Cc', ','.join(cc_list)) + reply.__setitem__('Content-Type', 'text/plain;') + reply.set_payload(self.ticket_data['description']) + + 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, "mq1-%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) + + # 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)) \ No newline at end of file From 99ae4bb801fa28f4910dd46718465f1a5c0327bc Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Tue, 15 Mar 2016 14:13:19 -0300 Subject: [PATCH 42/45] BUGFIX: Capture invalid email exceptions and ignore those inputs. --- helpdesk/forms.py | 2 +- helpdesk/management/commands/get_email.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/helpdesk/forms.py b/helpdesk/forms.py index d54f76a9..ee504dff 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -562,7 +562,7 @@ class TicketCCForm(forms.ModelForm): 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.')) + raise forms.ValidationError(_('When you add somebody on Cc, you must provided either an User or a valid email. Email: %s' %email)) class TicketDependencyForm(forms.ModelForm): class Meta: diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 96cef195..9f21f27c 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -24,6 +24,7 @@ from optparse import make_option from email_reply_parser import EmailReplyParser +from django.core.exceptions import ValidationError from django.core.files.base import ContentFile from django.core.management.base import BaseCommand from django.db.models import Q @@ -183,8 +184,11 @@ def create_ticket_cc(ticket, cc_list): except User.DoesNotExist: pass - ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email) - new_ticket_ccs.append(ticket_cc) + try: + ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email) + new_ticket_ccs.append(ticket_cc) + except ValidationError, err: + pass return new_ticket_ccs From 978e6d5c9bd94cc4317baf103815c793bf0c9eb0 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Thu, 17 Mar 2016 01:12:42 -0300 Subject: [PATCH 43/45] UPDATED: When notifying users about creation / updates on tickets, notify everybody using a single email message. --- helpdesk/forms.py | 10 -- helpdesk/management/commands/get_email.py | 36 ++-- helpdesk/tests/test_ticket_submission.py | 210 ++++++++++++++++------ helpdesk/views/staff.py | 25 +-- 4 files changed, 188 insertions(+), 93 deletions(-) diff --git a/helpdesk/forms.py b/helpdesk/forms.py index ee504dff..ecadc515 100644 --- a/helpdesk/forms.py +++ b/helpdesk/forms.py @@ -554,16 +554,6 @@ 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. Email: %s' %email)) - class TicketDependencyForm(forms.ModelForm): class Meta: model = TicketDependency diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 9f21f27c..e43217af 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -281,6 +281,14 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): new_ticket_ccs = create_ticket_cc(ticket, cc_list.split(',')) notification_template = None + notifications_to_be_sent = [sender_email,] + + if queue.enable_notifications_on_email_events and len(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 new: @@ -290,7 +298,7 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): send_templated_mail( 'newticket_submitter', context, - recipients=sender_email, + recipients=notifications_to_be_sent, sender=queue.from_address, fail_silently=True, extra_headers={'In-Reply-To': message_id}, @@ -344,24 +352,18 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): 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) + if queue.enable_notifications_on_email_events: - for email in ticket_cc_list : - notifications_to_be_sent.append(email) - - if queue.enable_notifications_on_email_events and 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}, - ) + if queue.updated_ticket_cc: + send_templated_mail( + 'updated_cc', + context, + recipients=notifications_to_be_sent, + sender=queue.from_address, + fail_silently=True, + ) return ticket diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index 3c871f2f..26844917 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -174,6 +174,9 @@ class EmailInteractionsTestCase(TestCase): # and the new and update queues (+2) self.assertEqual(email_count + 1 + 2, len(mail.outbox)) + # Ensure that the submitter is notified + self.assertIn(submitter_email,mail.outbox[0].to) + def test_create_ticket_from_email_without_message_id(self): """ @@ -203,6 +206,9 @@ class EmailInteractionsTestCase(TestCase): # and the new and update queues (+2) self.assertEqual(email_count + 1 + 2, len(mail.outbox)) + # Ensure that the submitter is notified + self.assertIn(submitter_email,mail.outbox[0].to) + def test_create_ticket_from_email_with_carbon_copy(self): """ @@ -232,13 +238,21 @@ class EmailInteractionsTestCase(TestCase): ticket = Ticket.objects.get(id=followup.ticket.id) self.assertEqual(ticket.ticket_for_url, "mq1-%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)) + # As we have created an Ticket from an email, we notify the sender + # and contacts on the cc_list (+1 as it's treated as a list), + # the new and update queues (+2) + self.assertEqual(email_count + 1 + 2, len(mail.outbox)) - # Ensure that is created + # Ensure that the submitter is notified + self.assertIn(submitter_email,mail.outbox[0].to) + + for cc_email in cc_list: + + # Ensure that contacts on cc_list will be notified on the same email (index 0) + self.assertIn(cc_email, mail.outbox[0].to) + + # Ensure that exists ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email) self.assertTrue(ticket_cc.ticket, ticket) self.assertTrue(ticket_cc.email, cc_email) @@ -266,9 +280,31 @@ class EmailInteractionsTestCase(TestCase): email_count = len(mail.outbox) - self.assertRaises(ValidationError, object_from_message, str(msg), self.queue_public, quiet=True) + 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): + followup = FollowUp.objects.get(message_id=message_id) + ticket = Ticket.objects.get(id=followup.ticket.id) + self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id) + + # As we have created an Ticket from an email, we notify the sender + # and contacts on the cc_list (+1 as it's treated as a list), + # the new and update queues (+2) + self.assertEqual(email_count + 1 + 2, len(mail.outbox)) + + # Ensure that the submitter is notified + self.assertIn(submitter_email, mail.outbox[0].to) + + for cc_email in cc_list: + + # Ensure that contacts on cc_list will be notified on the same email (index 0) + self.assertIn(cc_email, mail.outbox[0].to) + + # Ensure that exists. Even if it's an invalid email. + 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_no_initial_cc_list(self): """ Ensure that if a message is received with an valid In-Reply-To ID, @@ -295,6 +331,18 @@ class EmailInteractionsTestCase(TestCase): followup = FollowUp.objects.get(message_id=message_id) ticket = Ticket.objects.get(id=followup.ticket.id) + + # As we have created an Ticket from an email, we notify the sender + # and contacts on the cc_list (+1 as it's treated as a list), + # the new and update queues (+2) + + # Ensure that the submitter is notified + self.assertIn(submitter_email,mail.outbox[0].to) + + # 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 + self.assertEqual(expected_email_count, len(mail.outbox)) ### end of the Ticket and TicketCCs creation ### # Reply message @@ -327,16 +375,23 @@ class EmailInteractionsTestCase(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 + expected_email_count += 1 + 1 self.assertEqual(expected_email_count, len(mail.outbox)) + # As we have created a FollowUp from an email, we notify the sender + # and contacts on the cc_list (+1 as it's treated as a list), + # the new and update queues (+2) + + # Ensure that the submitter is notified + self.assertIn(submitter_email, mail.outbox[expected_email_count - 1].to) + + # Ensure that contacts on cc_list will be notified on the same email (index 0) + for cc_email in cc_list: + self.assertIn(cc_email, mail.outbox[expected_email_count - 1].to) + def test_create_followup_from_email_with_valid_message_id_with_original_cc_list_included(self): """ @@ -374,10 +429,17 @@ class EmailInteractionsTestCase(TestCase): 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)) + # As we have created a Ticket from an email, we notify the sender + # and contacts on the cc_list (+1 as it's treated as a list), + # the new and update queues (+2) + + # Ensure that the submitter is notified + self.assertIn(submitter_email,mail.outbox[0].to) + + # 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 + self.assertEqual(expected_email_count, len(mail.outbox)) ### end of the Ticket and TicketCCs creation ### # Reply message @@ -402,25 +464,28 @@ class EmailInteractionsTestCase(TestCase): ticket = Ticket.objects.get(id=followup.ticket.id) self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id) - # Ensure that is created + # As an update was made, we increase the expected_email_count with: + # public_update_queue: +1 + expected_email_count += 1 + self.assertEqual(expected_email_count, len(mail.outbox)) + + # As we have created a FollowUp from an email, we notify the sender + # and contacts on the cc_list (+1 as it's treated as a list), + # the new and update queues (+2) + + # Ensure that the submitter is notified + self.assertIn(submitter_email, mail.outbox[expected_email_count - 1].to) + + # Ensure that contacts on cc_list will be notified on the same email (index 0) for cc_email in cc_list: + self.assertIn(cc_email, mail.outbox[expected_email_count - 1].to) + # 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): """ @@ -458,10 +523,25 @@ class EmailInteractionsTestCase(TestCase): 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)) + # As we have created an Ticket from an email, we notify the sender + # and contacts on the cc_list (+1 as it's treated as a list), + # the new and update queues (+2) + expected_email_count = 1 + 2 + self.assertEqual(expected_email_count, len(mail.outbox)) + + # Ensure that the submitter is notified + self.assertIn(submitter_email,mail.outbox[0].to) + + # Ensure that is created + for cc_email in cc_list: + + # Ensure that contacts on cc_list will be notified on the same email (index 0) + self.assertIn(cc_email, mail.outbox[0].to) + + ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email) + self.assertTrue(ticket_cc.ticket, ticket) + self.assertTrue(ticket_cc.email, cc_email) + ### end of the Ticket and TicketCCs creation ### # Reply message @@ -491,6 +571,7 @@ class EmailInteractionsTestCase(TestCase): ticket = Ticket.objects.get(id=followup.ticket.id) self.assertEqual(ticket.ticket_for_url, "mq1-%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 @@ -502,7 +583,7 @@ class EmailInteractionsTestCase(TestCase): # 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)) + self.assertEqual(email_count + 1 + 2, len(mail.outbox)) def test_create_ticket_from_email_to_a_notification_enabled_queue(self): @@ -526,24 +607,32 @@ class EmailInteractionsTestCase(TestCase): 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, "mq1-%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)) + # As we have created a Ticket from an email, we notify the sender + # and contacts on the cc_list (+1 as it's treated as a list), + # the new and update queues (+2) + self.assertEqual(email_count + 1 + 2, len(mail.outbox)) - # Ensure that is created + # Ensure that the submitter is notified + self.assertIn(submitter_email, mail.outbox[0].to) + + # Ensure that exist for cc_email in cc_list: + + # Ensure that contacts on cc_list will be notified on the same email (index 0) + self.assertIn(cc_email, mail.outbox[0].to) + 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_to_a_notification_disabled_queue(self): """ @@ -581,6 +670,10 @@ class EmailInteractionsTestCase(TestCase): # Ensure that is created even if the Queue notifications are disabled # so when staff members interact with the , they get notified for cc_email in cc_list: + + # Ensure that contacts on the cc_list are not notified + self.assertNotIn(cc_email, mail.outbox[0].to) + ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email) self.assertTrue(ticket_cc.ticket, ticket) self.assertTrue(ticket_cc.email, cc_email) @@ -615,14 +708,18 @@ class EmailInteractionsTestCase(TestCase): ticket = Ticket.objects.get(id=followup.ticket.id) self.assertEqual(ticket.ticket_for_url, "mq1-%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) - expected_email_count = email_count + 1 + 2 + 1 + # As we have created an Ticket from an email, we notify the sender + # and contacts on the cc_list (+1 as it's treated as a list), + # the new and update queues (+2) + expected_email_count = email_count + 1 + 2 self.assertEqual(expected_email_count, len(mail.outbox)) # Ensure that is created for cc_email in cc_list: + + # Ensure that contacts on cc_list will be notified on the same email (index 0) + self.assertIn(cc_email, mail.outbox[0].to) + ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email) self.assertTrue(ticket_cc.ticket, ticket) self.assertTrue(ticket_cc.email, cc_email) @@ -649,11 +746,21 @@ class EmailInteractionsTestCase(TestCase): self.assertEqual(ticket.ticket_for_url, "mq1-%s" % ticket.id) # As an update was made, we increase the expected_email_count with: - # the ticket submitter: +1 + # a new email to all TicketCC subscribers : +1 # public_update_queue: +1 - expected_email_count += 1 + 1 + expected_email_count += 1 + 1 self.assertEqual(expected_email_count, len(mail.outbox)) + # Ensure that exist + for cc_email in cc_list: + + # Ensure that contacts on cc_list will be notified on the same email (index 0) + self.assertIn(cc_email, mail.outbox[expected_email_count - 1].to) + + 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_to_a_notification_diabled_queue(self): """ @@ -684,15 +791,19 @@ class EmailInteractionsTestCase(TestCase): ticket = Ticket.objects.get(id=followup.ticket.id) self.assertEqual(ticket.ticket_for_url, "mq1-%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) - expected_email_count = email_count + 1 + 2 + 1 + # As we have created an Ticket from an email, we notify the sender + # and contacts on the cc_list (+1 as it's treated as a list), + # the new and update queues (+2) + expected_email_count = email_count + 1 + 2 self.assertEqual(expected_email_count, len(mail.outbox)) # Ensure that is created for cc_email in cc_list: + + # Ensure that contacts on cc_list will be notified on the same email (index 0) + self.assertIn(cc_email, mail.outbox[0].to) + ticket_cc = TicketCC.objects.get(ticket=ticket, email=cc_email) self.assertTrue(ticket_cc.ticket, ticket) self.assertTrue(ticket_cc.email, cc_email) @@ -724,7 +835,6 @@ class EmailInteractionsTestCase(TestCase): self.assertEqual(expected_email_count, len(mail.outbox)) - def test_create_followup_from_email_with_valid_message_id_with_original_cc_list_included(self): """ diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 51969dd3..d9752392 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -340,30 +340,23 @@ def return_ticketccstring_and_show_subscribe(user, ticket): def subscribe_to_ticket_updates(ticket, user=None, email=None, can_view=True, can_update=False): - 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(): + if ticket is not None: queryset = TicketCC.objects.filter(ticket=ticket, user=user, email=email) + # Dont'create duplicate entries for subscribers if queryset.count() > 0: return queryset.first() - ticketcc = ticket_cc_form.save(commit=False) - ticketcc.ticket = ticket - ticketcc.save() - return ticketcc - else: - raise ValidationError( - _('Could not create subscribe contact to ticket updated. Errors: {}'.format(ticket_cc_form.errors)) + if user is None and len(email) < 5: + raise ValidationError( + _('When you add somebody on Cc, you must provided either an User or a valid email. Email: %s' %email) ) + ticketcc = TicketCC.objects.create(ticket=ticket, user=user, email=email,can_view=can_view, can_update=can_update) + + return ticketcc + def subscribe_staff_member_to_ticket(ticket, user, email=''): From 5933d2a59b267c33baa7459391814046f13992a7 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Thu, 17 Mar 2016 01:39:17 -0300 Subject: [PATCH 44/45] UPDATED: Strip inputed emails before creating TicketCC instances --- helpdesk/management/commands/get_email.py | 1 + 1 file changed, 1 insertion(+) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 44fb0505..70c48465 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -179,6 +179,7 @@ def create_ticket_cc(ticket, cc_list): for cced_email in cc_list: user = None + cced_email = cced_email.strip() try: user = User.objects.get(email=cced_email) From c5101a2873851316a69b3e59a61521bf15422621 Mon Sep 17 00:00:00 2001 From: Bruno Tikami Date: Wed, 23 Mar 2016 13:16:49 -0300 Subject: [PATCH 45/45] UP: When multiple emails are used on the "To" field, subscribe them as well. --- helpdesk/management/commands/get_email.py | 17 +++++-- helpdesk/tests/test_ticket_submission.py | 56 ++++++++++++++++++++++- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 70c48465..d159cbf5 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -12,6 +12,7 @@ scripts/get_email.py - Designed to be run from cron, this script checks the from __future__ import print_function import email +from email.utils import getaddresses import imaplib import mimetypes import poplib @@ -172,11 +173,17 @@ def decode_mail_headers(string): def create_ticket_cc(ticket, cc_list): + if not cc_list: + return [] + # 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: + for cced_name, cced_email in cc_list: + + if cced_email == ticket.queue.email_address: + continue user = None cced_email = cced_email.strip() @@ -202,9 +209,11 @@ def create_object_from_email_message(message, ticket_id, payload, files, quiet): queue = payload['queue'] sender_email = payload['sender_email'] + to_list = getaddresses(message.get_all('To', [])) + cc_list = getaddresses(message.get_all('Cc', [])) + message_id = message.get('Message-Id') in_reply_to = message.get('In-Reply-To') - cc_list = message.get('Cc') if in_reply_to is not None: try: @@ -279,8 +288,8 @@ 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: - new_ticket_ccs = create_ticket_cc(ticket, cc_list.split(',')) + new_ticket_ccs.append(create_ticket_cc(ticket, to_list)) + new_ticket_ccs.append(create_ticket_cc(ticket, cc_list)) notification_template = None notifications_to_be_sent = [sender_email,] diff --git a/helpdesk/tests/test_ticket_submission.py b/helpdesk/tests/test_ticket_submission.py index 26844917..3d831422 100644 --- a/helpdesk/tests/test_ticket_submission.py +++ b/helpdesk/tests/test_ticket_submission.py @@ -121,7 +121,8 @@ class EmailInteractionsTestCase(TestCase): def setUp(self): self.queue_public = Queue.objects.create(title='Mail Queue 1', - slug='mq1', + slug='mq1', + email_address='queue-1@example.com', allow_public_submission=True, new_ticket_cc='new.public.with.notifications@example.com', updated_ticket_cc='update.public.with.notifications@example.com', @@ -129,7 +130,8 @@ class EmailInteractionsTestCase(TestCase): ) self.queue_public_with_notifications_disabled = Queue.objects.create(title='Mail Queue 2', - slug='mq2', + slug='mq2', + email_address='queue-2@example.com', allow_public_submission=True, new_ticket_cc='new.public.without.notifications@example.com', updated_ticket_cc='update.public.without.notifications@example.com', @@ -247,6 +249,56 @@ class EmailInteractionsTestCase(TestCase): self.assertIn(submitter_email,mail.outbox[0].to) + for cc_email in cc_list: + + # Ensure that contacts on cc_list will be notified on the same email (index 0) + self.assertIn(cc_email, mail.outbox[0].to) + + # Ensure that exists + 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_to_multiple_emails(self): + + """ + Ensure that an instance of is created for every valid element of the + "rfc_2822_cc" field when creating a instance. + """ + + msg = email.message.Message() + + message_id = uuid.uuid4().hex + submitter_email = 'foo@bar.py' + to_list = [self.queue_public.email_address] + cc_list = ['bravo@example.net', 'charlie@foobar.com'] + + msg.__setitem__('Message-ID', message_id) + msg.__setitem__('Subject', self.ticket_data['title']) + msg.__setitem__('From', submitter_email) + msg.__setitem__('To', ','.join(to_list + cc_list)) + msg.__setitem__('Content-Type', 'text/plain;') + msg.set_payload(self.ticket_data['description']) + + 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, "mq1-%s" % ticket.id) + + # As we have created an Ticket from an email, we notify the sender + # and contacts on the cc_list (+1 as it's treated as a list), + # the new and update queues (+2) + self.assertEqual(email_count + 1 + 2, len(mail.outbox)) + + # Ensure that the submitter is notified + self.assertIn(submitter_email,mail.outbox[0].to) + + # Ensure that the queue's email was not subscribed to the event notifications. + self.assertRaises(TicketCC.DoesNotExist, TicketCC.objects.get, ticket=ticket, email=to_list[0]) + for cc_email in cc_list: # Ensure that contacts on cc_list will be notified on the same email (index 0)