-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add email reminder sent every 3 months to organizations admins …
…having multiple members
- Loading branch information
Showing
16 changed files
with
610 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
itou/companies/migrations/0003_company_active_members_email_reminder_last_sent_at.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
), | ||
), | ||
] |
53 changes: 53 additions & 0 deletions
53
itou/companies/migrations/0004_set_companies_active_members_email_reminder_last_sent_at.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
] |
19 changes: 19 additions & 0 deletions
19
itou/institutions/migrations/0004_institution_active_members_email_reminder_last_sent_at.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
), | ||
), | ||
] |
55 changes: 55 additions & 0 deletions
55
...stitutions/migrations/0005_set_institutions_active_members_email_reminder_last_sent_at.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
] |
19 changes: 19 additions & 0 deletions
19
...bers/migrations/0002_prescriberorganization_active_members_email_reminder_last_sent_at.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
), | ||
), | ||
] |
58 changes: 58 additions & 0 deletions
58
...igrations/0003_set_prescriber_organizations_active_members_email_reminder_last_sent_at.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
] |
17 changes: 17 additions & 0 deletions
17
itou/templates/users/emails/check_authorized_members_email_body.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 %} |
4 changes: 4 additions & 0 deletions
4
itou/templates/users/emails/check_authorized_members_email_subject.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
104
itou/users/management/commands/send_check_authorized_members_email.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"]) |
Oops, something went wrong.