Skip to content

Commit

Permalink
[Inbound Parsing] Modification du format d'email (#901)
Browse files Browse the repository at this point in the history
* add two new fields for uuids

* update logic email

* update migrations

* add tests and rename

* improve tests lisibility

* clean from review
  • Loading branch information
madjid-asa authored Sep 11, 2023
1 parent 6a46a71 commit 2e14a8a
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 15 deletions.
41 changes: 39 additions & 2 deletions lemarche/api/emails/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
from django.core import mail
from django.test import TestCase
from django.urls import reverse
from django.utils.text import slugify

from lemarche.conversations.factories import ConversationFactory
from lemarche.conversations.models import Conversation


class InboundEmailParsingApiTest(TestCase):
class InboundEmailParsingApiTestV0(TestCase):
@classmethod
def setUpTestData(cls):
cls.conv: Conversation = ConversationFactory()
cls.conv: Conversation = ConversationFactory(version=0)
email_data_file_url = os.path.join(os.path.dirname(__file__), "data_inbound_tests.json")

with open(email_data_file_url, "r") as file:
Expand All @@ -22,6 +23,20 @@ def setUpTestData(cls):
cls.item_email_data["To"][0]["Address"] = cls.conv.sender_email_buyer_encoded
cls.url = reverse("api:inbound-email-parsing")

def test_encoded_items(self):
# test sender and siae encoded format
# self.conv.uuid = "MjpaVwLGJBUwdKpkYJHqNr"
# sender_email_buyer_encoded= 'MjpaVwLGJBUwdKpkYJHqNr_b@reply.staging.lemarche.inclusion.beta.gouv.fr'
sender_encoded = self.conv.sender_email_buyer_encoded.split("@")[0]
self.assertEqual(sender_encoded.split("_")[0], self.conv.uuid)
sender_conv = Conversation.objects.get_conv_from_uuid(conv_uuid=sender_encoded.split("_")[0], version=0)
self.assertEqual(sender_conv, self.conv)
# sender_email_siae_encoded= 'MjpaVwLGJBUwdKpkYJHqNr_b@reply.staging.lemarche.inclusion.beta.gouv.fr'
siae_encoded = self.conv.sender_email_siae_encoded.split("@")[0]
self.assertEqual(siae_encoded.split("_")[0], self.conv.uuid)
siae_conv = Conversation.objects.get_conv_from_uuid(conv_uuid=siae_encoded.split("_")[0], version=0)
self.assertEqual(siae_conv, self.conv)

def test_inbound_serializer_parse_emails(self):
response = self.client.post(self.url, data=self.email_data, content_type="application/json")
self.assertEqual(response.status_code, 201)
Expand Down Expand Up @@ -73,3 +88,25 @@ def test_inbound_emails_send_to_siae(self):
f"{self.conv.sender_first_name} {self.conv.sender_last_name} <{self.conv.sender_email_buyer_encoded}>",
)
self.assertEqual(mail.outbox[0].to, [self.conv.sender_email_siae])


class InboundEmailParsingApiTest(InboundEmailParsingApiTestV0):
# Conversation V1 test
@classmethod
def setUpTestData(cls):
super().setUpTestData()
cls.conv.version = 1
cls.conv.save()

def test_encoded_items(self):
# test sender and siae encoded format
# self.conv.sender_encoded = 'alexandre_delahaye_du_wagner_yves_thomas_3b81'
# self.conv.sender_full_name = "Alexandre Delahaye du Wagner Yves Thomas"
self.assertEqual(self.conv.sender_encoded.split("_")[:-1], slugify(self.conv.sender_full_name).split("-"))
sender_conv = Conversation.objects.get_conv_from_uuid(conv_uuid=self.conv.sender_encoded, version=1)
self.assertEqual(sender_conv, self.conv)
# self.conv.siae_encoded = 'aime_bailly_josephine_vincent_perez_d243'
# self.conv.siae.contact_full_name = "Alexandre Delahaye du Wagner Yves Thomas"
self.assertEqual(self.conv.siae_encoded.split("_")[:-1], slugify(self.conv.siae.contact_full_name).split("-"))
siae_conv = Conversation.objects.get_conv_from_uuid(conv_uuid=self.conv.siae_encoded, version=1)
self.assertEqual(siae_conv, self.conv)
7 changes: 4 additions & 3 deletions lemarche/api/emails/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ def post(self, request):
serializer = EmailsSerializer(data=request.data)
if serializer.is_valid():
inboundEmail = serializer.validated_data.get("items")[0]
address_mail = inboundEmail["To"][0]["Address"]
address_mail_label = inboundEmail["To"][0]["Address"].split("@")[0]
# get conversation object
conv_uuid, user_kind = Conversation.get_email_info_from_address(address_mail)
conv: Conversation = Conversation.objects.get(uuid=conv_uuid)
version, conv_uuid, user_kind = Conversation.get_email_info_from_address(address_mail_label)
conv: Conversation = Conversation.objects.get_conv_from_uuid(conv_uuid=conv_uuid, version=version)
# save the input data
conv.data.append(serializer.data)
conv.save()
user_kind = user_kind if version == 0 else conv.get_user_kind(conv_uuid)
# make the transfert of emails
send_email_from_conversation(
conv=conv,
Expand Down
5 changes: 3 additions & 2 deletions lemarche/conversations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def queryset(self, request, queryset):

@admin.register(Conversation, site=admin_site)
class ConversationAdmin(admin.ModelAdmin):
list_display = ["id", "uuid", "is_validate", "title", "kind", "answer_count", "created_at"]
list_display = ["id", "uuid", "sender_encoded", "is_validate", "title", "kind", "answer_count", "created_at"]
list_filter = ["kind", HasAnswerFilter, IsValidatedFilter]
search_fields = ["id", "uuid", "sender_email"]
search_help_text = "Cherche sur les champs : ID, UUID, Initiateur (E-mail)"
Expand All @@ -54,6 +54,7 @@ class ConversationAdmin(admin.ModelAdmin):
readonly_fields = [
"id",
"uuid",
"sender_encoded",
"title",
"version",
"siae",
Expand All @@ -69,7 +70,7 @@ class ConversationAdmin(admin.ModelAdmin):
fieldsets = (
(
None,
{"fields": ("uuid", "title", "initial_body_message")},
{"fields": ("uuid", "sender_encoded", "title", "initial_body_message")},
),
("Interlocuteurs", {"fields": ("sender_first_name", "sender_last_name", "sender_email", "siae")}),
(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Generated by Django 4.2.2 on 2023-09-05 18:03
import uuid

from django.db import migrations, models


def set_uuids(apps, schema_editor):
Conversation = apps.get_model("conversations", "Conversation")
for conversation in Conversation.objects.all():
siae = conversation.siae
conversation.sender_encoded = (
f"{conversation.sender_first_name}_{conversation.sender_last_name}_{str(uuid.uuid4())[:4]}"
)
conversation.siae_encoded = f"{siae.contact_first_name}_{siae.contact_last_name}_{str(uuid.uuid4())[:4]}"
conversation.save()


class Migration(migrations.Migration):
dependencies = [
("conversations", "0009_conversation_validated_at"),
]

operations = [
migrations.AddField(
model_name="conversation",
name="sender_encoded",
field=models.CharField(db_index=True, default="", max_length=255, verbose_name="Identifiant initiateur"),
preserve_default=False,
),
migrations.AddField(
model_name="conversation",
name="siae_encoded",
field=models.CharField(db_index=True, default="", max_length=255, verbose_name="Identifiant structure"),
preserve_default=False,
),
migrations.AlterField(
model_name="conversation",
name="version",
field=models.PositiveIntegerField(default=1, verbose_name="Version"),
),
migrations.RunPython(set_uuids, migrations.RunPython.noop),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.2 on 2023-09-05 18:19

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("conversations", "0010_conversation_uuid_sender_conversation_uuid_siae_and_more"),
]

operations = [
migrations.AlterField(
model_name="conversation",
name="sender_encoded",
field=models.CharField(db_index=True, max_length=255, unique=True, verbose_name="Identifiant initiateur"),
),
migrations.AlterField(
model_name="conversation",
name="siae_encoded",
field=models.CharField(db_index=True, max_length=255, unique=True, verbose_name="Identifiant structure"),
),
]
98 changes: 90 additions & 8 deletions lemarche/conversations/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from uuid import uuid4

from django.conf import settings
from django.db import models
from django.db import IntegrityError, models
from django.db.models import Func, IntegerField
from django.utils import timezone
from django.utils.text import slugify
from django_extensions.db.fields import ShortUUIDField
from shortuuid import uuid

Expand All @@ -13,6 +16,19 @@ def has_answer(self):
def with_answer_count(self):
return self.annotate(answer_count=Func("data", function="jsonb_array_length", output_field=IntegerField()))

def get_conv_from_uuid(self, conv_uuid: str, version=1):
"""get conv form
Args:
conv_uuid (str): _description_
Returns:
[VERSION, UUID, KIND_SENDER]
"""
if version == 0:
return self.get(uuid=conv_uuid)
else:
return self.get(models.Q(sender_encoded=conv_uuid) | models.Q(siae_encoded=conv_uuid))


class Conversation(models.Model):
KIND_SEARCH = "SEARCH"
Expand All @@ -34,7 +50,11 @@ class Conversation(models.Model):
auto_created=True,
)

version = models.PositiveIntegerField(verbose_name="Version", default=0)
sender_encoded = models.CharField(
verbose_name="Identifiant initiateur", unique=True, db_index=True, max_length=255
)
siae_encoded = models.CharField(verbose_name="Identifiant structure", unique=True, db_index=True, max_length=255)
version = models.PositiveIntegerField(verbose_name="Version", default=1)

kind = models.CharField(
verbose_name="Type de conversation", default=KIND_SEARCH, choices=KIND_CHOICES, max_length=10, db_index=True
Expand Down Expand Up @@ -70,17 +90,72 @@ class Meta:
def __str__(self):
return self.title

def set_sender_encoded(self):
"""
The UUID of sender.
"""
if not self.sender_encoded:
slug_sender_full_name = slugify(self.sender_full_name).replace("-", "_")
self.sender_encoded = f"{slug_sender_full_name}_{str(uuid4())[:4]}"

def set_siae_encoded(self):
"""
The UUID of siae.
"""
if not self.siae_encoded:
siae_slug_full_name = slugify(self.siae.contact_full_name).replace("-", "_")
self.siae_encoded = f"{siae_slug_full_name}_{str(uuid4())[:4]}"

def save(self, *args, **kwargs):
"""
- generate the uuid field
"""
try:
self.set_sender_encoded()
self.set_siae_encoded()
super().save(*args, **kwargs)
except IntegrityError as e:
# check that it's a new UUID conflict
# Full message expected: duplicate key value violates unique constraint "conversations_conversation_sender_encoded_0f0b821f_uniq" DETAIL: Key (sender_encoded)=(...) already exists. # noqa
if "conversations_conversation_sender_encoded" in str(e):
self.set_sender_encoded()
super().save(*args, **kwargs)
if "conversations_conversation_siae_encoded" in str(e):
self.set_siae_encoded()
super().save(*args, **kwargs)
else:
raise e

def get_user_kind(self, conv_uuid):
# method only available in version >= 1
if conv_uuid == self.sender_encoded:
return self.USER_KIND_SENDER_TO_BUYER
elif conv_uuid == self.siae_encoded:
return self.USER_KIND_SENDER_TO_SIAE

@property
def sender_email_buyer(self):
return self.sender_email

@property
def sender_full_name(self):
return f"{self.sender_first_name} {self.sender_last_name}"

@property
def sender_email_buyer_encoded(self):
return f"{self.uuid}_{self.USER_KIND_SENDER_TO_BUYER}@{settings.INBOUND_PARSING_DOMAIN_EMAIL}"
if self.version == 0:
# for legacy
return f"{self.uuid}_{self.USER_KIND_SENDER_TO_BUYER}@{settings.INBOUND_PARSING_DOMAIN_EMAIL}"
if self.version == 1:
return f"{self.sender_encoded}@{settings.INBOUND_PARSING_DOMAIN_EMAIL}"

@property
def sender_email_siae_encoded(self):
return f"{self.uuid}_{self.USER_KIND_SENDER_TO_SIAE}@{settings.INBOUND_PARSING_DOMAIN_EMAIL}"
if self.version == 0:
# for legacy
return f"{self.uuid}_{self.USER_KIND_SENDER_TO_SIAE}@{settings.INBOUND_PARSING_DOMAIN_EMAIL}"
if self.version == 1:
return f"{self.siae_encoded}@{settings.INBOUND_PARSING_DOMAIN_EMAIL}"

@property
def sender_email_siae(self):
Expand All @@ -96,15 +171,22 @@ def nb_messages(self):
return len(self.data) + 1

@staticmethod
def get_email_info_from_address(address_mail: str) -> list:
def get_email_info_from_address(address_mail_label: str) -> list:
"""Extract info from address mail managed by this class
Args:
address_mail (str): _description_
address_mail_label (str): _description_
Returns:
[UUID, KIND_SENDER]
[VERSION, UUID, KIND_SENDER]
"""
return address_mail.split("@")[0].split("_")
email_infos = address_mail_label.split("_")
# version is 0 email is like "uuid_kind"
# version is 1 email is like "full_name_can_be_long_short_uuid"
version = 0 if len(email_infos) == 2 else 1
# in version 1 kind sender is not usefull
uuid = email_infos[0] if version == 0 else "_".join(email_infos)
kind_sender = email_infos[1] if version == 0 else None
return version, uuid, kind_sender

@property
def is_validated(self) -> bool:
Expand Down

0 comments on commit 2e14a8a

Please sign in to comment.