Skip to content

Commit

Permalink
feat: add email reminder sent every 3 months to organizations admins …
Browse files Browse the repository at this point in the history
…having multiple members
  • Loading branch information
leo-naeka committed Jun 27, 2024
1 parent ea7bd8c commit 8941072
Show file tree
Hide file tree
Showing 16 changed files with 610 additions and 3 deletions.
1 change: 1 addition & 0 deletions clevercloud/cron.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"30 1 * * * $ROOT/clevercloud/run_management_command.sh new_users_to_mailjet --wet-run",
"0 3 * * * $ROOT/clevercloud/run_management_command.sh clearsessions",
"15 5 * * * $ROOT/clevercloud/run_management_command.sh prolongation_requests_chores email_reminder --wet-run",
"0 9 * * * $ROOT/clevercloud/run_management_command.sh send_check_authorized_members_email",
"0 12 * * * $ROOT/clevercloud/run_management_command.sh evaluation_campaign_notify",
"30 20 * * * $ROOT/clevercloud/crons/populate_metabase_emplois.sh --daily",
"5 23 * * * $ROOT/clevercloud/run_management_command.sh archive_employee_records --wet-run",
Expand Down
5 changes: 5 additions & 0 deletions itou/common_apps/organizations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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,
Expand Down
1 change: 1 addition & 0 deletions itou/communications/dispatch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
JobSeekerNotification,
PrescriberNotification,
PrescriberOrEmployerNotification,
PrescriberOrEmployerOrLaborInspectorNotification,
WithStructureMixin,
)
7 changes: 7 additions & 0 deletions itou/communications/dispatch/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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"
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 5.0.6 on 2024-06-10 15:31

import datetime
import math
import time

from dateutil.relativedelta import relativedelta
from django.db import migrations


def set_active_members_email_reminder_last_sent_at(apps, schema_editor):
"""
Set active_members_email_reminder_last_sent_at for companies created more than 3 months ago.
Doing so to avoid processing a single and huge batch of most of the pre-existing companies during the first run of
the send_check_authorized_members_email management command.
"""
START_DATE = datetime.date.today() - relativedelta(months=3)
Company = apps.get_model("companies", "Company")

companies_qs = (
Company.objects.filter(
active_members_email_reminder_last_sent_at__isnull=True,
created_at__date__lt=START_DATE,
)
.only("active_members_email_reminder_last_sent_at", "created_at")
.order_by("created_at")
)
batch_size = max([math.ceil(companies_qs.count() / 90), 50])
batch_start_date = START_DATE

companies_processed = 0
start = time.perf_counter()
while companies_batch := companies_qs[:batch_size]:
companies = []
for company in companies_batch:
company.active_members_email_reminder_last_sent_at = datetime.datetime.combine(
batch_start_date,
company.created_at.timetz(),
)
companies.append(company)
companies_processed += Company.objects.bulk_update(companies, {"active_members_email_reminder_last_sent_at"})
batch_start_date += datetime.timedelta(days=1)
print(f"{companies_processed} companies migrated in {time.perf_counter() - start:.2f} sec")


class Migration(migrations.Migration):
dependencies = [
("companies", "0003_company_active_members_email_reminder_last_sent_at"),
]

operations = [
migrations.RunPython(set_active_members_email_reminder_last_sent_at, migrations.RunPython.noop, elidable=True),
]
Original file line number Diff line number Diff line change
@@ -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", "0003_institution_unique_ddets_per_department"),
]

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"
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Generated by Django 5.0.6 on 2024-06-10 15:31

import datetime
import math
import time

from dateutil.relativedelta import relativedelta
from django.db import migrations


def set_active_members_email_reminder_last_sent_at(apps, schema_editor):
"""
Set active_members_email_reminder_last_sent_at for institutions created more than 3 months ago.
Doing so to avoid processing a single and huge batch of most of the pre-existing institutions during the first run
of the send_check_authorized_members_email management command.
"""
START_DATE = datetime.date.today() - relativedelta(months=3)
Institution = apps.get_model("institutions", "Institution")

institutions_qs = (
Institution.objects.filter(
active_members_email_reminder_last_sent_at__isnull=True,
created_at__date__lt=START_DATE,
)
.only("active_members_email_reminder_last_sent_at", "created_at")
.order_by("created_at")
)
batch_size = max([math.ceil(institutions_qs.count() / 90), 50])
batch_start_date = START_DATE

institutions_processed = 0
start = time.perf_counter()
while institutions_batch := institutions_qs[:batch_size]:
institutions = []
for institution in institutions_batch:
institution.active_members_email_reminder_last_sent_at = datetime.datetime.combine(
batch_start_date,
institution.created_at.timetz(),
)
institutions.append(institution)
institutions_processed += Institution.objects.bulk_update(
institutions, {"active_members_email_reminder_last_sent_at"}
)
batch_start_date += datetime.timedelta(days=1)
print(f"{institutions_processed} institutions migrated in {time.perf_counter() - start:.2f} sec")


class Migration(migrations.Migration):
dependencies = [
("institutions", "0004_institution_active_members_email_reminder_last_sent_at"),
]

operations = [
migrations.RunPython(set_active_members_email_reminder_last_sent_at, migrations.RunPython.noop, elidable=True),
]
Original file line number Diff line number Diff line change
@@ -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"
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Generated by Django 5.0.6 on 2024-06-10 15:31

import datetime
import math
import time

from dateutil.relativedelta import relativedelta
from django.db import migrations


def set_active_members_email_reminder_last_sent_at(apps, schema_editor):
"""
Set active_members_email_reminder_last_sent_at for prescriber organizations created more than 3 months ago.
Doing so to avoid processing a single and huge batch of most of the pre-existing prescriber organizations during
the first run of the send_check_authorized_members_email management command.
"""
START_DATE = datetime.date.today() - relativedelta(months=3)
PrescriberOrganization = apps.get_model("prescribers", "PrescriberOrganization")

prescriber_organizations_qs = (
PrescriberOrganization.objects.filter(
active_members_email_reminder_last_sent_at__isnull=True,
created_at__date__lt=START_DATE,
)
.only("active_members_email_reminder_last_sent_at", "created_at")
.order_by("created_at")
)
batch_size = max([math.ceil(prescriber_organizations_qs.count() / 90), 50])
batch_start_date = START_DATE

prescriber_organizations_processed = 0
start = time.perf_counter()
while prescriber_organizations_batch := prescriber_organizations_qs[:batch_size]:
prescriber_organizations = []
for prescriber_organization in prescriber_organizations_batch:
prescriber_organization.active_members_email_reminder_last_sent_at = datetime.datetime.combine(
batch_start_date,
prescriber_organization.created_at.timetz(),
)
prescriber_organizations.append(prescriber_organization)
prescriber_organizations_processed += PrescriberOrganization.objects.bulk_update(
prescriber_organizations, {"active_members_email_reminder_last_sent_at"}
)
batch_start_date += datetime.timedelta(days=1)
print(
f"{prescriber_organizations_processed} prescriber organizations "
f"migrated in {time.perf_counter() - start:.2f} sec"
)


class Migration(migrations.Migration):
dependencies = [
("prescribers", "0002_prescriberorganization_active_members_email_reminder_last_sent_at"),
]

operations = [
migrations.RunPython(set_active_members_email_reminder_last_sent_at, migrations.RunPython.noop, elidable=True),
]
Original file line number Diff line number Diff line change
@@ -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”.

{% if active_admins_count == 1 %}Pour des raisons de sécurité et si la configuration de votre organisation vous le permet, nous vous invitons à nommer plusieurs administrateurs.

{% endif %}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 %}
Original file line number Diff line number Diff line change
@@ -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 %}
104 changes: 104 additions & 0 deletions itou/users/management/commands/send_check_authorized_members_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from dateutil.relativedelta import relativedelta
from django.db import transaction
from django.db.models import Count, Prefetch, Q
from django.db.models.functions import Coalesce
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 build_query(self, queryset):
TODAY = timezone.localdate()
membership_attname = queryset.model.members.through._meta.get_field("user").remote_field.name

return (
queryset.prefetch_related(
Prefetch(
"members",
queryset=User.objects.order_by("date_joined").filter(
**{
"is_active": True,
f"{membership_attname}__is_active": True,
f"{membership_attname}__is_admin": True,
},
),
to_attr="admin_members",
)
)
.annotate(
last_sent_at=Coalesce("active_members_email_reminder_last_sent_at", "created_at"),
active_members_count=Count(
"members",
filter=Q(
**{
f"{membership_attname}__is_active": True,
f"{membership_attname}__user__is_active": True,
}
),
),
)
.filter(
last_sent_at__date__lte=TODAY - relativedelta(months=3),
active_members_count__gt=1,
)
.order_by("pk")
)

def handle(self, *args, **options):
NOW = timezone.now()

# Companies
companies = self.build_query(Company.objects.active())
self.stdout.write(f"Processing {len(companies)} companies")
for company in companies:
with transaction.atomic():
for member in company.admin_members:
OrganizationActiveMembersReminderNotification(
member, company, active_admins_count=len(company.admin_members)
).send()
self.stdout.write(f" - Sent reminder notification to user #{member.pk} for company #{company.pk}")
company.active_members_email_reminder_last_sent_at = NOW
company.save(update_fields=["active_members_email_reminder_last_sent_at"])

# Prescriber organizations
prescriber_organizations = self.build_query(PrescriberOrganization.objects.all())
self.stdout.write(f"Processing {len(prescriber_organizations)} prescriber organizations")
for prescriber_organization in prescriber_organizations:
with transaction.atomic():
for member in prescriber_organization.admin_members:
OrganizationActiveMembersReminderNotification(
member, prescriber_organization, active_admins_count=len(prescriber_organization.admin_members)
).send()
self.stdout.write(
f" - Sent reminder notification to user #{member.pk} "
f"for prescriber organization #{prescriber_organization.pk}"
)
prescriber_organization.active_members_email_reminder_last_sent_at = NOW
prescriber_organization.save(update_fields=["active_members_email_reminder_last_sent_at"])

# Institutions
institutions = self.build_query(Institution.objects.all())
self.stdout.write(f"Processing {len(institutions)} institutions")
for institution in institutions:
with transaction.atomic():
for member in institution.admin_members:
OrganizationActiveMembersReminderNotification(
member, institution, active_admins_count=len(institution.admin_members)
).send()
self.stdout.write(
f" - Sent reminder notification to user #{member.pk} for institution #{institution.pk}"
)
institution.active_members_email_reminder_last_sent_at = NOW
institution.save(update_fields=["active_members_email_reminder_last_sent_at"])
Loading

0 comments on commit 8941072

Please sign in to comment.