Skip to content
This repository has been archived by the owner on Oct 22, 2024. It is now read-only.

Commit

Permalink
Merge branch 'main' into multiple-departments-management
Browse files Browse the repository at this point in the history
  • Loading branch information
ikarius authored Feb 29, 2024
2 parents a272f73 + 8fb7b25 commit 700d550
Show file tree
Hide file tree
Showing 13 changed files with 353 additions and 6 deletions.
3 changes: 1 addition & 2 deletions dora/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,7 @@ def get_thematiques(self, obj):
return [scat for scat in scats if not scat.endswith("--autre")]

def get_prise_rdv(self, obj):
# TODO: pas encore supporté sur DORA
return None
return obj.appointment_link

def get_frais(self, obj):
return obj.fee_condition.value if obj.fee_condition else None
Expand Down
3 changes: 2 additions & 1 deletion dora/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ def test_service_serialization_exemple(authenticated_user, api_client):
recurrence="Tu 09:00-12:00;We 14:00-17:00",
coach_orientation_modes_other="Mêmes modalités que pour les bénéficiaires",
beneficiaries_access_modes_other="Contacter conseiller(e) Pôle Emploi",
appointment_link="https://example.com",
)

service.subcategories.add(
Expand Down Expand Up @@ -259,7 +260,7 @@ def test_service_serialization_exemple(authenticated_user, api_client):
"pre_requis": ["Bonne connaissance du français oral et écrit"],
"presentation_detail": "Service de proximité visant à soutenir les familles ayant la responsabilité de jeunes enfants, en particulier les familles monoparentales.",
"presentation_resume": "Accompagnement des familles à domicile",
"prise_rdv": None,
"prise_rdv": "https://example.com",
"profils": ["adultes", "jeunes-16-26", "femmes"],
"recurrence": "Tu 09:00-12:00;We 14:00-17:00",
"source": None,
Expand Down
4 changes: 2 additions & 2 deletions dora/core/templates/email-base.mjml
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@
{% endblock %}

<!-- Post-CTA -->
{% block post_cta %}
{% endblock %}
{% block post_cta %}
{% endblock %}

<!-- Salutations -->
{% block signature %}
Expand Down
3 changes: 3 additions & 0 deletions dora/notifications/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class TaskType(models.TextChoices):
SERVICE_ACTIVATION = "service_activation"
INVITED_USERS = "invited_users"
SELF_INVITED_USERS = "self_invited_users"
USERS_WITHOUT_STRUCTURE = "users_without_structure"

...

# catch-all: pour des cas de tests, ou "one-shot"
GENERIC_TASK = "generic_task"
3 changes: 3 additions & 0 deletions dora/notifications/tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
from . import invitations # noqa
from . import structures # noqa
from . import users # noqa

# imports utilisés pour l'activation des tâches de notification (effet de bord)
117 changes: 117 additions & 0 deletions dora/notifications/tasks/tests/tests_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import uuid

import pytest
from dateutil.relativedelta import relativedelta
from django.core import mail
from django.utils import timezone
from freezegun import freeze_time

from dora.core.test_utils import make_structure, make_user
from dora.notifications.models import Notification
from dora.users.models import User

from ..core import Task
from ..users import UsersWithoutStructureTask


@pytest.fixture
def user_without_structure_task():
return UsersWithoutStructureTask()


def test_user_without_structure_task_is_registered():
assert UsersWithoutStructureTask in Task.registered_tasks()


def test_user_without_structure_task_candidates(user_without_structure_task):
# pas d'id IC
nok_user = make_user()
assert nok_user not in user_without_structure_task.candidates()

# adresse e-mail non validée
nok_user = make_user(is_valid=False)
assert nok_user not in user_without_structure_task.candidates()

# utilisateurs incrits depuis plus de 4 mois exclus
nok_user = make_user(
ic_id=uuid.uuid4(), date_joined=timezone.now() - relativedelta(months=4, days=1)
)
assert nok_user not in user_without_structure_task.candidates()

# membres de structure exclus
nok_user = make_user(structure=make_structure(), ic_id=uuid.uuid4())
assert nok_user not in user_without_structure_task.candidates()

# utilisateurs invités exclus
nok_user = make_user(ic_id=uuid.uuid4())
make_structure(putative_member=nok_user)
assert nok_user not in user_without_structure_task.candidates()

# candidat potentiel
ok_user = make_user(ic_id=uuid.uuid4())
assert ok_user in user_without_structure_task.candidates()


def test_user_without_structure_task_should_trigger(user_without_structure_task):
user = make_user(ic_id=uuid.uuid4())

# première notification à +1j
ok, _, _ = user_without_structure_task.run()
assert not ok

notification = Notification.objects.first()
assert notification.is_pending

now = timezone.now()

for cnt, day in enumerate((1, 5, 10, 15), 1):
with freeze_time(now + relativedelta(days=day)):
ok, _, _ = user_without_structure_task.run()
assert ok

notification.refresh_from_db()
assert notification.counter == cnt
assert notification.is_pending

# on vérifie qu'un e-mail est bien envoyé
# testé plus en détails dans la partie e-mail
assert len(mail.outbox) == cnt
assert mail.outbox[cnt - 1].to == [user.email]

# le contenu de l'e-mail est différent pour la dernière notification
match cnt:
case 4:
assert (
mail.outbox[cnt - 1].subject
== "Dernier rappel avant suppression"
)
case _:
assert (
mail.outbox[cnt - 1].subject
== "Rappel : Identifiez votre structure sur DORA"
)

with freeze_time(now + relativedelta(days=day + 1)):
ok, _, _ = user_without_structure_task.run()
assert not ok

notification.refresh_from_db()
assert notification.counter == cnt
assert notification.is_pending

notification.refresh_from_db()
assert notification.counter == 4
assert notification.is_pending

# on teste la dernière iteration de la notification (+4 mois)
with freeze_time(now + relativedelta(months=4)):
ok, _, _ = user_without_structure_task.run()
assert ok

# la notification ne doit plus exister (l'utilisateur propriétaire a été supprimé)
with pytest.raises(Notification.DoesNotExist):
notification.refresh_from_db()

# le compte utilisateur doit avoir été supprimé
with pytest.raises(User.DoesNotExist):
user.refresh_from_db()
82 changes: 82 additions & 0 deletions dora/notifications/tasks/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from dateutil.relativedelta import relativedelta
from django.utils import timezone

from dora.notifications.enums import TaskType
from dora.notifications.models import Notification
from dora.users.emails import send_user_without_structure_notification
from dora.users.models import User

from .core import Task, TaskError

"""
Users :
Notifications ayant pour candidats des utilisateurs "en direct",
c.a.d. sans passer par l'intermédiaire d'un objet tiers,
comme une invitation ou une structure.
"""


class UsersWithoutStructureTask(Task):
@classmethod
def task_type(cls):
return TaskType.USERS_WITHOUT_STRUCTURE

@classmethod
def candidates(cls):
return User.objects.exclude(ic_id=None).filter(
is_valid=True,
membership=None,
putative_membership=None,
date_joined__gt=timezone.now() - relativedelta(months=4, days=1),
)

@classmethod
def should_trigger(cls, notification: Notification) -> bool:
now = timezone.now()
match notification.counter:
case 0:
return notification.created_at + relativedelta(days=1) <= now
case 1:
return notification.created_at + relativedelta(days=5) <= now
case 2:
return notification.created_at + relativedelta(days=10) <= now
case 3:
return notification.created_at + relativedelta(days=15) <= now
case 4:
return notification.created_at + relativedelta(months=4) <= now
case _:
return False

@classmethod
def process(cls, notification: Notification):
match notification.counter:
case 0 | 1 | 2 | 3:
try:
send_user_without_structure_notification(
notification.owner_user, deletion=notification.counter == 3
)
except Exception as ex:
raise TaskError(
f"Erreur d'envoi du mail pour un utilisateur sans structure ({notification}) : {ex}"
) from ex
case 4:
notification.complete()
# action : voir post_process
case _:
raise TaskError(f"État du compteur incohérent ({notification})")

@classmethod
def post_process(cls, notification: Notification):
print("PP")
if notification.is_complete:
print("PP:deleting")
user = notification.owner_user
# suppression du compte utilisateur associé si :
# - aucune autre invitation
# - non membre d'une structure
if not user.putative_membership.count() and not user.membership.count():
user.delete()
# à ce point, la notification doit aussi être détruite (CASCADE)...


Task.register(UsersWithoutStructureTask)
19 changes: 19 additions & 0 deletions dora/services/migrations/0104_service_appointment_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.10 on 2024-02-29 16:04

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("services", "0103_savedsearch_location_kinds"),
]

operations = [
migrations.AddField(
model_name="service",
name="appointment_link",
field=models.URLField(
blank=True, verbose_name="Lien de prise de rendez-vous"
),
),
]
4 changes: 4 additions & 0 deletions dora/services/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,10 @@ class Service(ModerationMixin, models.Model):
verbose_name="Jusqu’au", null=True, blank=True, db_index=True
)

appointment_link = models.URLField(
verbose_name="Lien de prise de rendez-vous", blank=True
)

##########
# Metadata

Expand Down
32 changes: 32 additions & 0 deletions dora/users/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,35 @@ def send_invitation_reminder(user, structure):
user.email,
mjml2html(render_to_string("invitation_reminder.mjml", context)),
)


def send_user_without_structure_notification(user, deletion=False):
# même notification et contexte mais template différent si dernier rappel
cta_link = furl(settings.FRONTEND_URL) / "auth" / "rattachement"
cta_link.add(
{
"login_hint": iri_to_uri(user.email),
"mtm_campaign": "MailsTransactionnels",
"mtm_keyword": "InscritSansStructure",
}
)
context = {
"user": user,
"cta_link": cta_link.url,
"with_legal_info": True,
}

send_mail(
"Dernier rappel avant suppression"
if deletion
else "Rappel : Identifiez votre structure sur DORA",
user.email,
mjml2html(
render_to_string(
"notification_user_without_structure_deletion.mjml"
if deletion
else "notification_user_without_structure.mjml",
context,
)
),
)
34 changes: 34 additions & 0 deletions dora/users/templates/notification_user_without_structure.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{% extends "email-base.mjml" %}

{% block preview %}N'oubliez pas d'identifier et de rejoindre votre structure{% endblock %}

{% block content %}
<p>
<strong>Bonjour {{ user.last_name }},</strong>
</p>

<p>Pour tirer le meilleur parti de notre plateforme, il est temps d’identifier et rejoindre votre structure dès maintenant, pour bénéficier de toutes les fonctionnalités de DORA&nbsp;:</p>
{% endblock %}


{% block post_cta %}
<mj-text>
<p>
N'oubliez pas que l'identification de votre structure est essentielle pour :
<ul>
<li>mettre en avant les informations concernant votre structure</li>
<li>inviter vos collaborateurs</li>
<li>référencer son offre de services</li>
<li>accéder aux coordonner de tous vos partenaires</li>
<li>envoyer vos demandes d’orientation via DORA à vos partenaires</li>
</ul>
</p>

<p>Ne manquez pas cette opportunité de bénéficier pleinement de DORA !</p>
</mj-text>
{% endblock %}

{% block cta %}
<mj-button mj-class="cta" href="{{ cta_link }}">Identifiez et rejoignez votre structure</mj-button>
<mj-spacer height="24px"/>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% extends "email-base.mjml" %}

{% block preview %}Activez votre compte DORA avant qu'il ne soit supprimé{% endblock %}

{% block content %}
<p>
<strong>Bonjour {{ user.last_name }},</strong>
</p>

<p>Nous avons remarqué que vous n'avez toujours pas identifié et rejoint votre structure sur DORA.</p>

<p>Veuillez noter que sans action de votre part, votre compte sera supprimé dans <strong>4 mois</strong>.</p>

<p>Pour éviter cela, activez votre compte dès maintenant&nbsp;:</p>
{% endblock %}

{% block post_cta %}
<mj-text>
<p>
Nous vous remercions pour votre intérêt pour DORA et espérons vous voir bientôt parmi nos utilisateurs actifs.
</p>
</mj-text>
{% endblock %}

{% block cta %}
<mj-button mj-class="cta" href="{{ cta_link }}">Identifiez et rejoignez votre structure</mj-button>
<mj-spacer height="24px"/>
{% endblock %}
Loading

0 comments on commit 700d550

Please sign in to comment.