From 8ed09ab532c6ddb0ad47061e55c4a6d224ce761c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=CC=81o=20S?= Date: Wed, 1 May 2024 18:27:51 +0200 Subject: [PATCH] feat: add email reminder sent every 3 monthes to organizations admins having multiple members --- clevercloud/cron.json | 4 +- itou/common_apps/organizations/models.py | 5 + itou/communications/dispatch/__init__.py | 1 + itou/communications/dispatch/utils.py | 7 ++ ...ive_members_email_reminder_last_sent_at.py | 19 ++++ ...ive_members_email_reminder_last_sent_at.py | 19 ++++ ...ive_members_email_reminder_last_sent_at.py | 19 ++++ .../check_authorized_members_email_body.txt | 17 ++++ ...check_authorized_members_email_subject.txt | 4 + .../send_check_authorized_members_email.py | 92 +++++++++++++++++++ itou/users/notifications.py | 13 +++ 11 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 itou/companies/migrations/0003_company_active_members_email_reminder_last_sent_at.py create mode 100644 itou/institutions/migrations/0002_institution_active_members_email_reminder_last_sent_at.py create mode 100644 itou/prescribers/migrations/0002_prescriberorganization_active_members_email_reminder_last_sent_at.py create mode 100644 itou/templates/common/emails/check_authorized_members_email_body.txt create mode 100644 itou/templates/common/emails/check_authorized_members_email_subject.txt create mode 100644 itou/users/management/commands/send_check_authorized_members_email.py create mode 100644 itou/users/notifications.py diff --git a/clevercloud/cron.json b/clevercloud/cron.json index 300670ca91d..fe66843f8be 100644 --- a/clevercloud/cron.json +++ b/clevercloud/cron.json @@ -31,5 +31,7 @@ "0 0 1 * * $ROOT/clevercloud/run_management_command.sh sync_cities --wet-run", "0 0 2 * * $ROOT/clevercloud/crons/populate_metabase_emplois.sh --monthly", - "0 0 15 * * $ROOT/clevercloud/run_management_command.sh sync_romes_and_appellations --wet-run" + "0 0 15 * * $ROOT/clevercloud/run_management_command.sh sync_romes_and_appellations --wet-run", + + "0 9 1-7 */3 1 $ROOT/clevercloud/run_management_command.sh send_check_authorized_members_email" ] diff --git a/itou/common_apps/organizations/models.py b/itou/common_apps/organizations/models.py index 9829af6ed87..bb9500aadc6 100644 --- a/itou/common_apps/organizations/models.py +++ b/itou/common_apps/organizations/models.py @@ -39,6 +39,11 @@ class OrganizationAbstract(models.Model): # This enables us to keep our internal primary key opaque and independent from any external logic. uid = models.UUIDField(db_index=True, default=uuid.uuid4, unique=True) + active_members_email_reminder_last_sent_at = models.DateTimeField( + null=True, + verbose_name="date d'envoi du dernier rappel pour vérifier les membres actifs", + ) + # Child class should have a "members" attribute, for example: # members = models.ManyToManyField( # settings.AUTH_USER_MODEL, diff --git a/itou/communications/dispatch/__init__.py b/itou/communications/dispatch/__init__.py index 6aa8356a2ac..0810a631e9a 100644 --- a/itou/communications/dispatch/__init__.py +++ b/itou/communications/dispatch/__init__.py @@ -6,5 +6,6 @@ JobSeekerNotification, PrescriberNotification, PrescriberOrEmployerNotification, + PrescriberOrEmployerOrLaborInspectorNotification, WithStructureMixin, ) diff --git a/itou/communications/dispatch/utils.py b/itou/communications/dispatch/utils.py index 94f98b1031a..b84b851a87a 100644 --- a/itou/communications/dispatch/utils.py +++ b/itou/communications/dispatch/utils.py @@ -18,6 +18,13 @@ def is_manageable_by_user(self): return super().is_manageable_by_user() and (self.user.is_prescriber or self.user.is_employer) +class PrescriberOrEmployerOrLaborInspectorNotification: + def is_manageable_by_user(self): + return super().is_manageable_by_user() and ( + self.user.is_prescriber or self.user.is_employer or self.user.is_labor_inspector + ) + + class WithStructureMixin: def is_manageable_by_user(self): return super().is_manageable_by_user() and self.structure is not None diff --git a/itou/companies/migrations/0003_company_active_members_email_reminder_last_sent_at.py b/itou/companies/migrations/0003_company_active_members_email_reminder_last_sent_at.py new file mode 100644 index 00000000000..f2b927f6f8d --- /dev/null +++ b/itou/companies/migrations/0003_company_active_members_email_reminder_last_sent_at.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.6 on 2024-06-05 09:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("companies", "0002_fix_job_app_contract_type_enum"), + ] + + operations = [ + migrations.AddField( + model_name="company", + name="active_members_email_reminder_last_sent_at", + field=models.DateTimeField( + null=True, verbose_name="date d'envoi du dernier rappel pour vérifier les membres actifs" + ), + ), + ] diff --git a/itou/institutions/migrations/0002_institution_active_members_email_reminder_last_sent_at.py b/itou/institutions/migrations/0002_institution_active_members_email_reminder_last_sent_at.py new file mode 100644 index 00000000000..e729bdd7455 --- /dev/null +++ b/itou/institutions/migrations/0002_institution_active_members_email_reminder_last_sent_at.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.6 on 2024-06-05 09:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("institutions", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="institution", + name="active_members_email_reminder_last_sent_at", + field=models.DateTimeField( + null=True, verbose_name="date d'envoi du dernier rappel pour vérifier les membres actifs" + ), + ), + ] diff --git a/itou/prescribers/migrations/0002_prescriberorganization_active_members_email_reminder_last_sent_at.py b/itou/prescribers/migrations/0002_prescriberorganization_active_members_email_reminder_last_sent_at.py new file mode 100644 index 00000000000..ed7a3a2c7de --- /dev/null +++ b/itou/prescribers/migrations/0002_prescriberorganization_active_members_email_reminder_last_sent_at.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.6 on 2024-06-05 09:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("prescribers", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="prescriberorganization", + name="active_members_email_reminder_last_sent_at", + field=models.DateTimeField( + null=True, verbose_name="date d'envoi du dernier rappel pour vérifier les membres actifs" + ), + ), + ] diff --git a/itou/templates/common/emails/check_authorized_members_email_body.txt b/itou/templates/common/emails/check_authorized_members_email_body.txt new file mode 100644 index 00000000000..0c468efc051 --- /dev/null +++ b/itou/templates/common/emails/check_authorized_members_email_body.txt @@ -0,0 +1,17 @@ +{% extends "layout/base_email_text_body.txt" %} +{% load format_filters %} +{% block body %} +Bonjour, + +En tant qu’administrateur de l’organisation {{ structure.name }}, nous vous invitons à vérifier la liste des membres afin de vous assurer que seuls les collaborateurs qui travaillent au sein de cette organisation puissent accéder à votre espace de travail. + +RDV sur votre espace des emplois de l’inclusion à la rubrique “Gérer les collaborateurs” : + +Si un collaborateur a quitté votre organisation, vous devez le retirer des membres en cliquant sur le bouton d’action situé à droite, puis sur l’option “retirer de la structure”. + +Pour des raisons de sécurité et si la configuration de votre organisation vous le permet, nous vous invitons à nommer plusieurs administrateurs. + +Ce rappel automatique vous sera transmis tous les 3 mois, mais il est vivement recommandé d’effectuer cette action dès qu’un collaborateur quitte votre organisation. + +Cordialement, +{% endblock body %} diff --git a/itou/templates/common/emails/check_authorized_members_email_subject.txt b/itou/templates/common/emails/check_authorized_members_email_subject.txt new file mode 100644 index 00000000000..6e97a1a9695 --- /dev/null +++ b/itou/templates/common/emails/check_authorized_members_email_subject.txt @@ -0,0 +1,4 @@ +{% extends "layout/base_email_text_subject.txt" %} +{% block subject %} +Rappel sécurité : vérifiez la liste des membres de l’organisation {{ structure.name }} +{% endblock %} diff --git a/itou/users/management/commands/send_check_authorized_members_email.py b/itou/users/management/commands/send_check_authorized_members_email.py new file mode 100644 index 00000000000..ffb51037b75 --- /dev/null +++ b/itou/users/management/commands/send_check_authorized_members_email.py @@ -0,0 +1,92 @@ +import datetime + +from django.db import transaction +from django.db.models import Count, IntegerField, Prefetch, Q +from django.db.models.functions import Coalesce, ExtractDay, ExtractMonth, Least, Mod +from django.utils import timezone + +from itou.companies.models import Company +from itou.institutions.models import Institution +from itou.prescribers.models import PrescriberOrganization +from itou.users.models import User +from itou.users.notifications import OrganizationActiveMembersReminderNotification +from itou.utils.command import BaseCommand + + +class Command(BaseCommand): + """ + Send an email reminder every 3 months asking admins of companies, organizations and institutions + having more than 1 member to review members access and ensure that only authorized members have + access to the organization data. + """ + + def handle(self, *args, **options): + TODAY = datetime.date.today() + + def get_query_for_model(model): + membership_attname = model.members.through._meta.get_field("user").remote_field.name + return ( + model.objects.active() + .prefetch_related( + Prefetch( + "members", + queryset=User.objects.filter( + **{ + "is_active": True, + f"{membership_attname}__is_active": True, + f"{membership_attname}__is_admin": True, + }, + ), + to_attr="admin_members", + ) + ) + .annotate( + last_sent_month_mod3=Mod( + ExtractMonth(Coalesce("active_members_email_reminder_last_sent_at", "created_at")), + 3, + output_field=IntegerField(), + ), + last_sent_day=Least( + ExtractDay(Coalesce("active_members_email_reminder_last_sent_at", "created_at")), 28 + ), + active_members_count=Count( + "members", + filter=Q( + **{ + f"{membership_attname}__is_active": True, + f"{membership_attname}__user__is_active": True, + } + ), + ), + ) + .filter( + last_sent_month_mod3=TODAY.month % 3, + last_sent_day=min(TODAY.day, 28), + active_members_count__gt=1, + ) + .order_by("pk") + ) + + # Companies + for company in get_query_for_model(Company): + with transaction.atomic(): + for member in self.admin_members: + OrganizationActiveMembersReminderNotification(member, company).send() + company.active_members_email_reminder_last_sent_at = timezone.now() + company.save(update_fields=["active_members_email_reminder_last_sent_at"]) + + # Prescriber organizations + for prescriber_organization in get_query_for_model(PrescriberOrganization): + with transaction.atomic(): + for member in self.admin_members: + OrganizationActiveMembersReminderNotification(member, prescriber_organization).send() + prescriber_organization.active_members_email_reminder_last_sent_at = timezone.now() + prescriber_organization.save(update_fields=["active_members_email_reminder_last_sent_at"]) + + # Institutions + for institution in get_query_for_model(Institution): + with transaction.atomic(): + for member in self.admin_members: + OrganizationActiveMembersReminderNotification(member, institution).send() + institution.active_members_email_reminder_last_sent_at = timezone.now() + institution.save(update_fields=["active_members_email_reminder_last_sent_at"]) diff --git a/itou/users/notifications.py b/itou/users/notifications.py new file mode 100644 index 00000000000..b738f0bffd8 --- /dev/null +++ b/itou/users/notifications.py @@ -0,0 +1,13 @@ +from itou.communications import NotificationCategory, registry as notifications_registry +from itou.communications.dispatch import EmailNotification, PrescriberOrEmployerOrLaborInspectorNotification + + +@notifications_registry.register +class OrganizationActiveMembersReminderNotification( + PrescriberOrEmployerOrLaborInspectorNotification, EmailNotification +): + name = "Rappel périodique pour s'assurer que les membres de sa structure sont bien actifs et autorisés" + category = NotificationCategory.MEMBERS_MANAGEMENT + subject_template = "users/email/member_deactivation_email_subject.txt" + body_template = "users/email/member_deactivation_email_body.txt" + can_be_disabled = False