Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Candidature: Prévenir dans la candidature que la personne a quitté l’organisation #4916

Merged
merged 3 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions itou/communications/dispatch/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ class BaseNotification:

can_be_disabled = True

def __init__(self, user, structure=None, /, **kwargs):
def __init__(self, user, structure=None, forward_from_user=None, /, **kwargs):
self.user = user
self.structure = structure
self.forward_from_user = forward_from_user
self.context = kwargs

def __repr__(self):
Expand All @@ -29,7 +30,11 @@ def should_send(self):
return True

def get_context(self):
return self.validate_context()
return self.validate_context() | {
"user": self.user,
"structure": self.structure,
xavfernandez marked this conversation as resolved.
Show resolved Hide resolved
"forward_from_user": self.forward_from_user,
}

def validate_context(self):
return self.context
33 changes: 23 additions & 10 deletions itou/communications/dispatch/email.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging

from itou.companies.models import CompanyMembership
from itou.prescribers.models import PrescriberMembership
from itou.utils.emails import get_email_message

Expand All @@ -13,15 +14,6 @@ class EmailNotification(BaseNotification):
REQUIRED = BaseNotification.REQUIRED + ["subject_template", "body_template"]

tonial marked this conversation as resolved.
Show resolved Hide resolved
def build(self):
# TODO: Temporary log for analysis : remove by the end of November 2024
if self.user.is_prescriber and self.structure:
memberships = (
PrescriberMembership.objects.active().filter(organization=self.structure).select_related("user")
)
members = [m.user for m in memberships]
if self.user not in members:
admin_count = len([m for m in memberships if m.is_admin])
logger.info("Estimate new email sent to admin_count=%d", admin_count)
return get_email_message(
[self.user.email],
self.get_context(),
Expand All @@ -30,5 +22,26 @@ def build(self):
)

def send(self):
if (
# If it is already a forwarded notification, do not check if the user is still a member of the organization
not self.forward_from_user
# Don't use should_send() if the user left the org because we don't want to use his settings
and self.is_applicable()
and self.structure
and (self.user.is_prescriber or self.user.is_employer)
):
if self.user.is_prescriber:
memberships = (
PrescriberMembership.objects.active().filter(organization=self.structure).select_related("user")
)
elif self.user.is_employer:
memberships = CompanyMembership.objects.active().filter(company=self.structure).select_related("user")
members = [m.user for m in memberships]
if self.user not in members:
admins = [m.user for m in memberships if m.is_admin]
logger.info("Send email copy to admin, admin_count=%d", len(admins))
for admin in admins:
self.__class__(admin, self.structure, self.user, **self.context).send()
return
if self.should_send():
return self.build().send()
self.build().send()
17 changes: 17 additions & 0 deletions itou/templates/apply/includes/job_application_sender_info.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
{% load format_filters %}
{% load matomo %}

{% if job_application_sender_left_org %}
<div class="alert alert-warning alert-dismissible fade show" role="status">
tonial marked this conversation as resolved.
Show resolved Hide resolved
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Fermer"></button>
<div class="row">
<div class="col-auto pe-0">
<i class="ri-information-line ri-xl text-warning" aria-hidden="true"></i>
</div>
<div class="col">
<p class="mb-0">L’émetteur de cette candidature ne fait plus partie de l’organisation émettrice</p>
</div>
</div>
</div>


{% endif %}
<ul class="list-data list-data__two-column-md mb-3">
<li>
<small>Émetteur</small>
Expand All @@ -14,6 +29,8 @@
<small>Adresse e-mail</small>
{% if request.user.is_job_seeker and job_application.sender_kind != SenderKind.JOB_SEEKER %}
<strong>Non communiquée</strong>
{% elif job_application_sender_left_org %}
<div class="text-warning fst-italic">Les réponses seront transmises aux administrateurs de l’organisation</div>
tonial marked this conversation as resolved.
Show resolved Hide resolved
{% else %}
<strong>{{ job_application.sender.email }}</strong>
{% matomo_event "candidature" "clic" "copied_sender_email" as matomo_event_attrs %}
Expand Down
6 changes: 6 additions & 0 deletions itou/templates/layout/base_email_text_body.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{% autoescape off %}

{% if forward_from_user|default:False %}
Vous recevez cet e-mail parce que l'utilisateur {{ forward_from_user.get_full_name }} ({{ forward_from_user.email}}) ne fait plus partie de votre {{ forward_from_user.is_employer|yesno:"structure,organisation" }}.

-----
{% endif %}

{% block body %}{% endblock %}

{% include "layout/base_email_signature.txt" %}
Expand Down
5 changes: 0 additions & 5 deletions itou/users/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ class OrganizationActiveMembersReminderNotification(
body_template = "users/emails/check_authorized_members_email_body.txt"
can_be_disabled = False

def get_context(self):
context = super().get_context()
context["structure"] = self.structure
return context


@notifications_registry.register
class JobSeekerCreatedByProxyNotification(EmailNotification):
Expand Down
11 changes: 11 additions & 0 deletions itou/www/apply/views/process_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ def _get_geiq_eligibility_diagnosis(job_application, only_prescriber):
).first()


def job_application_sender_left_org(job_app):
if org_id := job_app.sender_prescriber_organization_id:
return not job_app.sender.prescribermembership_set.active().filter(organization_id=org_id).exists()
if company_id := job_app.sender_company_id:
return not job_app.sender.companymembership_set.active().filter(company_id=company_id).exists()
return False


@login_required
def details_for_jobseeker(request, job_application_id, template_name="apply/process_details.html"):
"""
Expand Down Expand Up @@ -156,6 +164,7 @@ def details_for_jobseeker(request, job_application_id, template_name="apply/proc
"transition_logs": transition_logs,
"back_url": back_url,
"matomo_custom_title": "Candidature",
"job_application_sender_left_org": job_application_sender_left_org(job_application),
}

return render(request, template_name, context)
Expand Down Expand Up @@ -267,6 +276,7 @@ def details_for_company(request, job_application_id, template_name="apply/proces
PriorActionForm(action_only=True) if job_application.can_change_prior_actions else None
),
"matomo_custom_title": "Candidature",
"job_application_sender_left_org": job_application_sender_left_org(job_application),
}

return render(request, template_name, context)
Expand Down Expand Up @@ -349,6 +359,7 @@ def details_for_prescriber(request, job_application_id, template_name="apply/pro
"refused_by": refused_by,
"refusal_contact_email": refusal_contact_email,
"with_job_seeker_detail_url": True,
"job_application_sender_left_org": job_application_sender_left_org(job_application),
}

return render(request, template_name, context)
Expand Down
85 changes: 84 additions & 1 deletion tests/communications/test_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
WithStructureMixin,
)
from itou.communications.models import NotificationRecord, NotificationSettings
from tests.companies.factories import CompanyMembershipFactory
from tests.prescribers.factories import PrescriberMembershipFactory
from tests.users.factories import EmployerFactory, JobSeekerFactory, PrescriberFactory


Expand Down Expand Up @@ -166,8 +168,15 @@ def test_method_should_send(
assert not manageable_non_applicable_notification(self.user, self.organization).should_send()

def test_method_get_context(self):
assert BaseNotification(self.user, self.organization).get_context() == {}
assert BaseNotification(self.user, self.organization).get_context() == {
"user": self.user,
"structure": self.organization,
"forward_from_user": None,
}
assert BaseNotification(self.user, self.organization, kw1=1, kw2=2).get_context() == {
"user": self.user,
"structure": self.organization,
"forward_from_user": None,
"kw1": 1,
"kw2": 2,
}
Expand Down Expand Up @@ -209,6 +218,80 @@ def test_method_send(self, email_notification, django_capture_on_commit_callback
assert mailoutbox[0].to == [self.user.email]
assert "Cet email est envoyé depuis un environnement de démonstration" in mailoutbox[0].body

def test_method_send_for_prescriber_that_left_his_org(
self, email_notification, django_capture_on_commit_callbacks, mailoutbox, caplog
):
self.user.prescribermembership_set.update(is_active=False)

admin_1 = PrescriberMembershipFactory(
user=PrescriberFactory(),
organization=self.organization,
is_admin=True,
).user
admin_2 = PrescriberMembershipFactory(
user=PrescriberFactory(),
organization=self.organization,
is_admin=True,
).user
PrescriberMembershipFactory(
user=PrescriberFactory(),
organization=self.organization,
is_admin=False,
)

with django_capture_on_commit_callbacks(execute=True):
email_notification(self.user, self.organization).send()

assert caplog.messages == ["Send email copy to admin, admin_count=2"]
assert len(mailoutbox) == 2
assert set(mailoutbox[0].to + mailoutbox[1].to) == {admin_1.email, admin_2.email}
assert (
f"Vous recevez cet e-mail parce que l'utilisateur {self.user.get_full_name()} ({self.user.email})"
" ne fait plus partie de votre organisation." in mailoutbox[0].body
)
assert (
f"Vous recevez cet e-mail parce que l'utilisateur {self.user.get_full_name()} ({self.user.email})"
" ne fait plus partie de votre organisation." in mailoutbox[1].body
)

def test_method_send_for_employer_that_left_his_company(
self, email_notification, django_capture_on_commit_callbacks, mailoutbox, caplog
):
user = EmployerFactory(with_company=True)
company = user.companymembership_set.first().company
user.companymembership_set.update(is_active=False)

admin_1 = CompanyMembershipFactory(
user=EmployerFactory(),
company=company,
is_admin=True,
).user
admin_2 = CompanyMembershipFactory(
user=EmployerFactory(),
company=company,
is_admin=True,
).user
CompanyMembershipFactory(
user=EmployerFactory(),
company=company,
is_admin=False,
)

with django_capture_on_commit_callbacks(execute=True):
email_notification(user, company).send()

assert caplog.messages == ["Send email copy to admin, admin_count=2"]
assert len(mailoutbox) == 2
assert set(mailoutbox[0].to + mailoutbox[1].to) == {admin_1.email, admin_2.email}
assert (
f"Vous recevez cet e-mail parce que l'utilisateur {user.get_full_name()} ({user.email})"
" ne fait plus partie de votre structure." in mailoutbox[0].body
)
assert (
f"Vous recevez cet e-mail parce que l'utilisateur {user.get_full_name()} ({user.email})"
" ne fait plus partie de votre structure." in mailoutbox[1].body
)


class TestProfiledNotification:
def setup_method(self):
Expand Down
58 changes: 57 additions & 1 deletion tests/job_applications/__snapshots__/test_transfer.ambr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# serializer version: 1
# name: test_model_fields
dict({
'num_queries': 14,
'num_queries': 15,
'queries': list([
dict({
'origin': list([
Expand Down Expand Up @@ -147,6 +147,62 @@
ORDER BY RANDOM() ASC
''',
}),
dict({
'origin': list([
'JobApplicationTransferredForEmployerNotification.send[communications/dispatch/email.py]',
'JobApplication.transfer[job_applications/models.py]',
]),
'sql': '''
SELECT "companies_companymembership"."id",
"companies_companymembership"."user_id",
"companies_companymembership"."joined_at",
"companies_companymembership"."is_admin",
"companies_companymembership"."is_active",
"companies_companymembership"."created_at",
"companies_companymembership"."updated_at",
"companies_companymembership"."company_id",
"companies_companymembership"."updated_by_id",
"companies_companymembership"."notifications",
"users_user"."id",
"users_user"."password",
"users_user"."last_login",
"users_user"."is_superuser",
"users_user"."username",
"users_user"."first_name",
"users_user"."last_name",
"users_user"."is_staff",
"users_user"."is_active",
"users_user"."date_joined",
"users_user"."address_line_1",
"users_user"."address_line_2",
"users_user"."post_code",
"users_user"."city",
"users_user"."department",
"users_user"."coords",
"users_user"."geocoding_score",
"users_user"."geocoding_updated_at",
"users_user"."ban_api_resolved_address",
"users_user"."insee_city_id",
"users_user"."title",
"users_user"."email",
"users_user"."phone",
"users_user"."kind",
"users_user"."identity_provider",
"users_user"."has_completed_welcoming_tour",
"users_user"."created_by_id",
"users_user"."external_data_source_history",
"users_user"."last_checked_at",
"users_user"."public_id",
"users_user"."address_filled_at",
"users_user"."first_login"
FROM "companies_companymembership"
INNER JOIN "users_user" ON ("companies_companymembership"."user_id" = "users_user"."id")
WHERE ("users_user"."is_active"
AND "companies_companymembership"."is_active"
AND "companies_companymembership"."company_id" = %s)
ORDER BY RANDOM() ASC
''',
}),
dict({
'origin': list([
'JobApplicationTransferredForEmployerNotification.should_send[communications/dispatch/base.py]',
Expand Down
Loading
Loading