From eb154116af8233f599e267e51b98e921e4cc041a Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Wed, 29 Jan 2025 22:25:53 +0100 Subject: [PATCH 01/10] gps: Add new field to FollowUpGroupMembership This will allow us to track the last contact the user made with the job seeker, allowing a better display order and a mean to archive old memberships --- itou/gps/admin.py | 9 ++++++++- ...pgroupmembership_last_contact_at_and_more.py | 17 +++++++++++++++++ itou/gps/models.py | 2 ++ tests/gps/__snapshots__/test_views.ambr | 15 +++++++++------ 4 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 itou/gps/migrations/0006_followupgroupmembership_last_contact_at_and_more.py diff --git a/itou/gps/admin.py b/itou/gps/admin.py index 7f32979609..901061721d 100644 --- a/itou/gps/admin.py +++ b/itou/gps/admin.py @@ -37,7 +37,14 @@ class FollowUpGroupMembershipAdmin(ItouModelAdmin): "created_in_bulk", ) raw_id_fields = ("follow_up_group", "member") - readonly_fields = ("creator", "created_at", "updated_at", "ended_at", "created_in_bulk") + readonly_fields = ( + "creator", + "created_at", + "last_contact_at", + "updated_at", + "ended_at", + "created_in_bulk", + ) ordering = ["-created_at"] def get_readonly_fields(self, request, obj=None): diff --git a/itou/gps/migrations/0006_followupgroupmembership_last_contact_at_and_more.py b/itou/gps/migrations/0006_followupgroupmembership_last_contact_at_and_more.py new file mode 100644 index 0000000000..2e259838a3 --- /dev/null +++ b/itou/gps/migrations/0006_followupgroupmembership_last_contact_at_and_more.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.5 on 2025-01-27 10:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("gps", "0005_francetravailcontact"), + ] + + operations = [ + migrations.AddField( + model_name="followupgroupmembership", + name="last_contact_at", + field=models.DateTimeField(null=True, verbose_name="date de dernier contact"), + ), + ] diff --git a/itou/gps/models.py b/itou/gps/models.py index 2fe6f269da..3532aedd88 100644 --- a/itou/gps/models.py +++ b/itou/gps/models.py @@ -100,6 +100,8 @@ class Meta: created_at = models.DateTimeField(verbose_name="date de création", default=timezone.now) created_in_bulk = models.BooleanField(verbose_name="créé massivement", default=False, db_index=True) + last_contact_at = models.DateTimeField(verbose_name="date de dernier contact", null=True) + # Keep track of when the membership was ended ended_at = models.DateTimeField(verbose_name="date de désactivation", null=True) diff --git a/tests/gps/__snapshots__/test_views.ambr b/tests/gps/__snapshots__/test_views.ambr index 7f4b0b5470..ed8168a4c2 100644 --- a/tests/gps/__snapshots__/test_views.ambr +++ b/tests/gps/__snapshots__/test_views.ambr @@ -659,6 +659,7 @@ "gps_followupgroupmembership"."is_active", "gps_followupgroupmembership"."created_at", "gps_followupgroupmembership"."created_in_bulk", + "gps_followupgroupmembership"."last_contact_at", "gps_followupgroupmembership"."ended_at", "gps_followupgroupmembership"."updated_at", "gps_followupgroupmembership"."follow_up_group_id", @@ -770,7 +771,8 @@ "col7", "col8", "col9", - "col10" + "col10", + "col11" FROM (SELECT * FROM @@ -779,11 +781,12 @@ "gps_followupgroupmembership"."is_active" AS "col3", "gps_followupgroupmembership"."created_at" AS "col4", "gps_followupgroupmembership"."created_in_bulk" AS "col5", - "gps_followupgroupmembership"."ended_at" AS "col6", - "gps_followupgroupmembership"."updated_at" AS "col7", - "gps_followupgroupmembership"."follow_up_group_id" AS "col8", - "gps_followupgroupmembership"."member_id" AS "col9", - "gps_followupgroupmembership"."creator_id" AS "col10", + "gps_followupgroupmembership"."last_contact_at" AS "col6", + "gps_followupgroupmembership"."ended_at" AS "col7", + "gps_followupgroupmembership"."updated_at" AS "col8", + "gps_followupgroupmembership"."follow_up_group_id" AS "col9", + "gps_followupgroupmembership"."member_id" AS "col10", + "gps_followupgroupmembership"."creator_id" AS "col11", ROW_NUMBER() OVER (PARTITION BY "gps_followupgroupmembership"."follow_up_group_id" ORDER BY RANDOM() ASC) AS "qual0", RANDOM() AS "qual1" From 1441f63bb691b1fc75ae29c6e27ce5b7e66f4f45 Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Thu, 30 Jan 2025 09:19:03 +0100 Subject: [PATCH 02/10] tests: improve gps test using refresh_from_db() --- tests/gps/test_models.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/gps/test_models.py b/tests/gps/test_models.py index 9af01c05f9..f5835d0aff 100644 --- a/tests/gps/test_models.py +++ b/tests/gps/test_models.py @@ -38,8 +38,7 @@ def test_follow_beneficiary(): membership.save() FollowUpGroup.objects.follow_beneficiary(beneficiary=beneficiary, user=prescriber, is_referent=True) - group = FollowUpGroup.objects.get() - membership = group.memberships.get() + membership.refresh_from_db() assert membership.is_active is True assert membership.is_referent is True @@ -47,8 +46,7 @@ def test_follow_beneficiary(): membership.save() FollowUpGroup.objects.follow_beneficiary(beneficiary=beneficiary, user=prescriber, is_referent=False) - group = FollowUpGroup.objects.get() - membership = group.memberships.get() + membership.refresh_from_db() assert membership.is_active is True assert membership.is_referent is False From 2a3468e81ea9231e77376582c20b7d4f3647595c Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Thu, 30 Jan 2025 09:23:22 +0100 Subject: [PATCH 03/10] gps: Stop naming the arguments when calling follow_beneficiary --- itou/www/job_seekers_views/views.py | 10 +++------- tests/gps/test_models.py | 8 ++++---- tests/gps/test_views.py | 4 ++-- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/itou/www/job_seekers_views/views.py b/itou/www/job_seekers_views/views.py index c5343cc450..f82c243c5f 100644 --- a/itou/www/job_seekers_views/views.py +++ b/itou/www/job_seekers_views/views.py @@ -432,9 +432,7 @@ def post(self, request, *args, **kwargs): # The NIR we found is correct if self.form.data.get("confirm"): if self.is_gps: - FollowUpGroup.objects.follow_beneficiary( - beneficiary=job_seeker, user=request.user, is_referent=True - ) + FollowUpGroup.objects.follow_beneficiary(job_seeker, request.user, is_referent=True) return HttpResponseRedirect(self.get_exit_url(job_seeker.public_id)) context = { @@ -522,9 +520,7 @@ def post(self, request, *args, **kwargs): logger.exception("step_job_seeker: error when saving job_seeker=%s nir=%s", job_seeker, nir) else: if self.is_gps: - FollowUpGroup.objects.follow_beneficiary( - beneficiary=job_seeker, user=request.user, is_referent=True - ) + FollowUpGroup.objects.follow_beneficiary(job_seeker, request.user, is_referent=True) return HttpResponseRedirect(self.get_exit_url(job_seeker.public_id)) return self.render_to_response( @@ -796,7 +792,7 @@ def post(self, request, *args, **kwargs): url = self.get_exit_url(self.profile.user.public_id, created=True) if self.is_gps: - FollowUpGroup.objects.follow_beneficiary(beneficiary=user, user=request.user, is_referent=True) + FollowUpGroup.objects.follow_beneficiary(user, request.user, is_referent=True) return HttpResponseRedirect(url) diff --git a/tests/gps/test_models.py b/tests/gps/test_models.py index f5835d0aff..49ed9f24ac 100644 --- a/tests/gps/test_models.py +++ b/tests/gps/test_models.py @@ -26,7 +26,7 @@ def test_follow_beneficiary(): beneficiary = JobSeekerFactory() prescriber = PrescriberFactory(membership=True) - FollowUpGroup.objects.follow_beneficiary(beneficiary=beneficiary, user=prescriber, is_referent=True) + FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber, is_referent=True) group = FollowUpGroup.objects.get() membership = group.memberships.get() assert membership.is_active is True @@ -37,7 +37,7 @@ def test_follow_beneficiary(): membership.is_referent = False membership.save() - FollowUpGroup.objects.follow_beneficiary(beneficiary=beneficiary, user=prescriber, is_referent=True) + FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber, is_referent=True) membership.refresh_from_db() assert membership.is_active is True assert membership.is_referent is True @@ -45,13 +45,13 @@ def test_follow_beneficiary(): membership.is_active = False membership.save() - FollowUpGroup.objects.follow_beneficiary(beneficiary=beneficiary, user=prescriber, is_referent=False) + FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber, is_referent=False) membership.refresh_from_db() assert membership.is_active is True assert membership.is_referent is False other_member = EmployerFactory() - FollowUpGroup.objects.follow_beneficiary(beneficiary=beneficiary, user=other_member, is_referent=True) + FollowUpGroup.objects.follow_beneficiary(beneficiary, other_member, is_referent=True) assert group.memberships.count() == 2 other_membership = group.memberships.get(member=other_member) assert other_membership.is_referent is True # No limit to the number of referent diff --git a/tests/gps/test_views.py b/tests/gps/test_views.py index bc7d0b82aa..6362d270e0 100644 --- a/tests/gps/test_views.py +++ b/tests/gps/test_views.py @@ -86,7 +86,7 @@ def test_my_groups(snapshot, client): member__first_name="John", member__last_name="Doe", ) - FollowUpGroup.objects.follow_beneficiary(beneficiary=group.beneficiary, user=user) + FollowUpGroup.objects.follow_beneficiary(group.beneficiary, user) with assertSnapshotQueries(snapshot): response = client.get(reverse("gps:my_groups")) @@ -101,7 +101,7 @@ def test_my_groups(snapshot, client): # Test `is_referent` display. group = FollowUpGroupFactory(memberships=1, beneficiary__first_name="Janis", beneficiary__last_name="Joplin") - FollowUpGroup.objects.follow_beneficiary(beneficiary=group.beneficiary, user=user, is_referent=True) + FollowUpGroup.objects.follow_beneficiary(group.beneficiary, user, is_referent=True) response = client.get(reverse("gps:my_groups")) assertContains(response, "vous êtes référent") From 584a731e12461b65c0ca28066de8f3f52dba7b69 Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Wed, 29 Jan 2025 22:35:02 +0100 Subject: [PATCH 04/10] gps: Use update_or_create in follow_beneficiary --- itou/gps/models.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/itou/gps/models.py b/itou/gps/models.py index 3532aedd88..fca5f9e393 100644 --- a/itou/gps/models.py +++ b/itou/gps/models.py @@ -18,16 +18,15 @@ def follow_beneficiary(self, beneficiary, user, is_referent=False): with transaction.atomic(): group, _ = FollowUpGroup.objects.get_or_create(beneficiary=beneficiary) - updated = FollowUpGroupMembership.objects.filter(member=user, follow_up_group=group).update( - is_active=True, is_referent=is_referent + update_args = {"is_active": True, "is_referent": is_referent} + create_args = update_args | {"creator": user} + + FollowUpGroupMembership.objects.update_or_create( + follow_up_group=group, + member=user, + defaults=update_args, + create_defaults=create_args, ) - if not updated: - FollowUpGroupMembership.objects.create( - follow_up_group=group, - member=user, - creator=user, - is_referent=is_referent, - ) class FollowUpGroupQueryset(BulkCreatedAtQuerysetProxy, models.QuerySet): From 52ab19e56fc88b801705d89f8c137ff5a6e3ec8b Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Wed, 29 Jan 2025 22:42:45 +0100 Subject: [PATCH 05/10] gps: Make is_referent optionnal in follow_beneficiary --- itou/gps/models.py | 6 ++++-- tests/gps/test_models.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/itou/gps/models.py b/itou/gps/models.py index fca5f9e393..fcca951d22 100644 --- a/itou/gps/models.py +++ b/itou/gps/models.py @@ -14,11 +14,13 @@ def not_bulk_created(self): class FollowUpGroupManager(models.Manager): - def follow_beneficiary(self, beneficiary, user, is_referent=False): + def follow_beneficiary(self, beneficiary, user, is_referent=None): with transaction.atomic(): group, _ = FollowUpGroup.objects.get_or_create(beneficiary=beneficiary) - update_args = {"is_active": True, "is_referent": is_referent} + update_args = {"is_active": True} + if is_referent is not None: + update_args["is_referent"] = is_referent create_args = update_args | {"creator": user} FollowUpGroupMembership.objects.update_or_create( diff --git a/tests/gps/test_models.py b/tests/gps/test_models.py index 49ed9f24ac..b8cda91ffc 100644 --- a/tests/gps/test_models.py +++ b/tests/gps/test_models.py @@ -50,6 +50,17 @@ def test_follow_beneficiary(): assert membership.is_active is True assert membership.is_referent is False + FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber) + membership.refresh_from_db() + assert membership.is_referent is False # does not change + + membership.is_referent = True + membership.save() + + FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber) + membership.refresh_from_db() + assert membership.is_referent is True # does not change + other_member = EmployerFactory() FollowUpGroup.objects.follow_beneficiary(beneficiary, other_member, is_referent=True) assert group.memberships.count() == 2 From 4a367429dcb4b42ee6ecb6fe07c7063c0bb2820d Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Thu, 30 Jan 2025 06:50:39 +0100 Subject: [PATCH 06/10] gps: Update last_contact_at in follow_beneficiary We don't use updated_at as it might change when performing another action because of that auto_now --- itou/gps/models.py | 12 +++++- tests/gps/test_models.py | 90 ++++++++++++++++++++++------------------ 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/itou/gps/models.py b/itou/gps/models.py index fcca951d22..51dd3369ef 100644 --- a/itou/gps/models.py +++ b/itou/gps/models.py @@ -15,13 +15,21 @@ def not_bulk_created(self): class FollowUpGroupManager(models.Manager): def follow_beneficiary(self, beneficiary, user, is_referent=None): + now = timezone.now() with transaction.atomic(): group, _ = FollowUpGroup.objects.get_or_create(beneficiary=beneficiary) - update_args = {"is_active": True} + update_args = { + "is_active": True, + "last_contact_at": now, + } if is_referent is not None: update_args["is_referent"] = is_referent - create_args = update_args | {"creator": user} + + create_args = update_args | { + "creator": user, + "created_at": now, + } FollowUpGroupMembership.objects.update_or_create( follow_up_group=group, diff --git a/tests/gps/test_models.py b/tests/gps/test_models.py index b8cda91ffc..1c03627473 100644 --- a/tests/gps/test_models.py +++ b/tests/gps/test_models.py @@ -1,4 +1,6 @@ import pytest +from django.utils import timezone +from freezegun import freeze_time from pytest_django.asserts import assertNumQueries from itou.gps.models import FollowUpGroup, FollowUpGroupMembership @@ -26,46 +28,54 @@ def test_follow_beneficiary(): beneficiary = JobSeekerFactory() prescriber = PrescriberFactory(membership=True) - FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber, is_referent=True) - group = FollowUpGroup.objects.get() - membership = group.memberships.get() - assert membership.is_active is True - assert membership.is_referent is True - assert membership.creator == prescriber - - membership.is_active = False - membership.is_referent = False - membership.save() - - FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber, is_referent=True) - membership.refresh_from_db() - assert membership.is_active is True - assert membership.is_referent is True - - membership.is_active = False - membership.save() - - FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber, is_referent=False) - membership.refresh_from_db() - assert membership.is_active is True - assert membership.is_referent is False - - FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber) - membership.refresh_from_db() - assert membership.is_referent is False # does not change - - membership.is_referent = True - membership.save() - - FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber) - membership.refresh_from_db() - assert membership.is_referent is True # does not change - - other_member = EmployerFactory() - FollowUpGroup.objects.follow_beneficiary(beneficiary, other_member, is_referent=True) - assert group.memberships.count() == 2 - other_membership = group.memberships.get(member=other_member) - assert other_membership.is_referent is True # No limit to the number of referent + with freeze_time() as frozen_time: + created_at = timezone.now() + FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber, is_referent=True) + group = FollowUpGroup.objects.get() + membership = group.memberships.get() + assert membership.is_active is True + assert membership.is_referent is True + assert membership.created_at == created_at + assert membership.last_contact_at == created_at + assert membership.creator == prescriber + + membership.is_active = False + membership.is_referent = False + membership.save() + frozen_time.tick() + updated_at = timezone.now() + + FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber, is_referent=True) + membership.refresh_from_db() + assert membership.is_active is True + assert membership.is_referent is True + assert membership.created_at == created_at + assert membership.last_contact_at == updated_at + + membership.is_active = False + membership.save() + + FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber, is_referent=False) + membership.refresh_from_db() + assert membership.is_active is True + assert membership.is_referent is False + + FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber) + membership.refresh_from_db() + assert membership.is_referent is False # does not change + + membership.is_referent = True + membership.save() + + FollowUpGroup.objects.follow_beneficiary(beneficiary, prescriber) + membership.refresh_from_db() + assert membership.is_referent is True # does not change + + other_member = EmployerFactory() + FollowUpGroup.objects.follow_beneficiary(beneficiary, other_member, is_referent=True) + assert group.memberships.count() == 2 + other_membership = group.memberships.get(member=other_member) + assert other_membership.is_referent is True # No limit to the number of referent @pytest.mark.parametrize( From 1ecbde21cef0c8f2b075ec8673847a4cdc35c110 Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Thu, 30 Jan 2025 14:18:13 +0100 Subject: [PATCH 07/10] gps: Follow job seeker when creating a geiq eligibility diagnosis --- itou/eligibility/models/geiq.py | 4 ++++ tests/eligibility/test_geiq.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/itou/eligibility/models/geiq.py b/itou/eligibility/models/geiq.py index 8d0cbc1b2f..8876d2dded 100644 --- a/itou/eligibility/models/geiq.py +++ b/itou/eligibility/models/geiq.py @@ -15,6 +15,7 @@ CommonEligibilityDiagnosisQuerySet, ) from itou.eligibility.utils import geiq_allowance_amount +from itou.gps.models import FollowUpGroup from itou.prescribers.models import PrescriberOrganization from itou.users.models import User @@ -207,6 +208,9 @@ def create_eligibility_diagnosis( if administrative_criteria: result.administrative_criteria.set(administrative_criteria) + # Sync GPS groups + FollowUpGroup.objects.follow_beneficiary(job_seeker, author) + return result @classmethod diff --git a/tests/eligibility/test_geiq.py b/tests/eligibility/test_geiq.py index 47c69f0eb6..e6b0aec98a 100644 --- a/tests/eligibility/test_geiq.py +++ b/tests/eligibility/test_geiq.py @@ -7,6 +7,7 @@ from itou.companies.enums import CompanyKind from itou.eligibility.enums import AdministrativeCriteriaAnnex, AdministrativeCriteriaLevel from itou.eligibility.models import GEIQAdministrativeCriteria, GEIQEligibilityDiagnosis +from itou.gps.models import FollowUpGroup, FollowUpGroupMembership from tests.companies.factories import CompanyWithMembershipAndJobsFactory from tests.eligibility.factories import GEIQEligibilityDiagnosisFactory from tests.job_applications.factories import JobApplicationFactory @@ -53,6 +54,11 @@ def test_create_geiq_eligibility_diagnosis(administrative_criteria_annex_1): ) assert diagnosis.pk + group = FollowUpGroup.objects.get() + assert group.beneficiary == diagnosis.job_seeker + membership = FollowUpGroupMembership.objects.get(follow_up_group=group) + assert membership.member == diagnosis.author + assert membership.creator == diagnosis.author diagnosis = GEIQEligibilityDiagnosis.create_eligibility_diagnosis( job_seeker=JobSeekerFactory(), @@ -62,6 +68,11 @@ def test_create_geiq_eligibility_diagnosis(administrative_criteria_annex_1): ) assert diagnosis.pk + group = FollowUpGroup.objects.exclude(pk=group.pk).get() # Get the newer group + assert group.beneficiary == diagnosis.job_seeker + membership = FollowUpGroupMembership.objects.get(follow_up_group=group) + assert membership.member == diagnosis.author + assert membership.creator == diagnosis.author # bad cops: From d38787d4c0c8280d6834ac5540930e32b0b5c999 Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Thu, 30 Jan 2025 14:18:25 +0100 Subject: [PATCH 08/10] gps: Follow job seeker when creating a iae eligibility diagnosis --- itou/eligibility/models/iae.py | 5 +++++ tests/eligibility/test_iae.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/itou/eligibility/models/iae.py b/itou/eligibility/models/iae.py index 04d695cd6e..a231ea714f 100644 --- a/itou/eligibility/models/iae.py +++ b/itou/eligibility/models/iae.py @@ -17,6 +17,7 @@ AdministrativeCriteriaQuerySet, CommonEligibilityDiagnosisQuerySet, ) +from itou.gps.models import FollowUpGroup logger = logging.getLogger(__name__) @@ -214,6 +215,10 @@ def create_diagnosis(cls, job_seeker, *, author, author_organization, administra ) if administrative_criteria: diagnosis.administrative_criteria.add(*administrative_criteria) + + # Sync GPS groups + FollowUpGroup.objects.follow_beneficiary(job_seeker, author) + return diagnosis @classmethod diff --git a/tests/eligibility/test_iae.py b/tests/eligibility/test_iae.py index 4db20b98d8..2c9821fc6a 100644 --- a/tests/eligibility/test_iae.py +++ b/tests/eligibility/test_iae.py @@ -19,6 +19,7 @@ AdministrativeCriteriaQuerySet, ) from itou.eligibility.models.geiq import GEIQAdministrativeCriteria +from itou.gps.models import FollowUpGroup, FollowUpGroupMembership from itou.utils.mocks.api_particulier import ( rsa_certified_mocker, rsa_not_certified_mocker, @@ -276,6 +277,14 @@ def test_create_diagnosis_employer(self): assert diagnosis.administrative_criteria.count() == 0 assert diagnosis.expires_at == datetime.date(2025, 3, 5) + # Check GPS group + # ---------------------------------------------------------------------- + group = FollowUpGroup.objects.get() + assert group.beneficiary == job_seeker + membership = FollowUpGroupMembership.objects.get(follow_up_group=group) + assert membership.member == user + assert membership.creator == user + @freeze_time("2024-12-03") def test_create_diagnosis_prescriber(self): job_seeker = JobSeekerFactory() @@ -296,6 +305,14 @@ def test_create_diagnosis_prescriber(self): assert diagnosis.administrative_criteria.count() == 0 assert diagnosis.expires_at == datetime.date(2025, 6, 3) + # Check GPS group + # ---------------------------------------------------------------------- + group = FollowUpGroup.objects.get() + assert group.beneficiary == job_seeker + membership = FollowUpGroupMembership.objects.get(follow_up_group=group) + assert membership.member == prescriber + assert membership.creator == prescriber + def test_create_diagnosis_with_administrative_criteria(self): job_seeker = JobSeekerFactory() prescriber_organization = PrescriberOrganizationWithMembershipFactory(authorized=True) From aebb1b3a0d5fd533642d1260be3639cd9273fdba Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Thu, 30 Jan 2025 15:38:42 +0100 Subject: [PATCH 09/10] gps: Follow job seeker when applying for him If he's not the sender --- itou/www/apply/views/submit_views.py | 5 +++++ tests/www/apply/test_submit.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/itou/www/apply/views/submit_views.py b/itou/www/apply/views/submit_views.py index 57fb6be7b8..de5284db5d 100644 --- a/itou/www/apply/views/submit_views.py +++ b/itou/www/apply/views/submit_views.py @@ -17,6 +17,7 @@ from itou.eligibility.models import EligibilityDiagnosis from itou.eligibility.models.geiq import GEIQEligibilityDiagnosis from itou.files.models import File +from itou.gps.models import FollowUpGroup from itou.job_applications.models import JobApplication from itou.users.enums import UserKind from itou.users.models import User @@ -663,6 +664,10 @@ def form_valid(self): # The job application is now saved in DB, delete the session early to avoid any problems self.apply_session.delete() + if self.request.user.kind in [UserKind.EMPLOYER, UserKind.PRESCRIBER]: + # New job application -> sync GPS groups if the sender is not a jobseeker + FollowUpGroup.objects.follow_beneficiary(self.job_seeker, self.request.user) + try: # Send notifications company_recipients = User.objects.filter( diff --git a/tests/www/apply/test_submit.py b/tests/www/apply/test_submit.py index d621ff15bf..f7333730fe 100644 --- a/tests/www/apply/test_submit.py +++ b/tests/www/apply/test_submit.py @@ -29,6 +29,7 @@ GEIQAdministrativeCriteria, GEIQEligibilityDiagnosis, ) +from itou.gps.models import FollowUpGroup, FollowUpGroupMembership from itou.job_applications.enums import JobApplicationState, QualificationLevel, QualificationType, SenderKind from itou.job_applications.models import JobApplication from itou.siae_evaluations.models import Sanctions @@ -503,6 +504,9 @@ def test_apply_as_jobseeker(self, client, pdf_file): # + 1 in the page content assertContains(response, reverse("dashboard:edit_user_info"), count=3) + # GPS : a job seeker must not follow himself + assert not FollowUpGroup.objects.exists() + def test_apply_as_job_seeker_temporary_nir(self, client): """ Full path is tested above. See test_apply_as_job_seeker. @@ -1374,6 +1378,14 @@ def test_apply_as_authorized_prescriber(self, client, pdf_file): response = client.get(next_url) assert response.status_code == 200 + # Check GPS group + # ---------------------------------------------------------------------- + group = FollowUpGroup.objects.get() + assert group.beneficiary == new_job_seeker + membership = FollowUpGroupMembership.objects.get(follow_up_group=group) + assert membership.member == user + assert membership.creator == user + def test_cannot_create_job_seeker_with_pole_emploi_email(self, client): company = CompanyMembershipFactory().company @@ -1845,6 +1857,14 @@ def test_apply_as_prescriber(self, client, pdf_file): response = client.get(next_url) assert response.status_code == 200 + # Check GPS group + # ---------------------------------------------------------------------- + group = FollowUpGroup.objects.get() + assert group.beneficiary == new_job_seeker + membership = FollowUpGroupMembership.objects.get(follow_up_group=group) + assert membership.member == user + assert membership.creator == user + def test_check_info_as_prescriber_for_job_seeker_with_incomplete_info(self, client): company = CompanyFactory(with_membership=True, with_jobs=True, romes=("N1101", "N1105")) user = PrescriberFactory() @@ -2415,6 +2435,14 @@ def _test_apply_as_company(self, client, user, company, dummy_job_seeker, pdf_fi response = client.get(next_url) assert response.status_code == 200 + # Check GPS group + # ---------------------------------------------------------------------- + group = FollowUpGroup.objects.get() + assert group.beneficiary == new_job_seeker + membership = FollowUpGroupMembership.objects.get(follow_up_group=group) + assert membership.member == user + assert membership.creator == user + @pytest.mark.ignore_unknown_variable_template_error("confirmation_needed", "job_seeker") def test_apply_as_employer(self, client, pdf_file): company = CompanyWithMembershipAndJobsFactory(romes=("N1101", "N1105")) From 936306edbf1734d432727b3e99ef9a2567a76f36 Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Thu, 30 Jan 2025 14:48:55 +0100 Subject: [PATCH 10/10] gps: follow job seeker when accepting a job application --- itou/job_applications/models.py | 4 ++++ tests/gps/test_management_commands.py | 22 +++++++++++++++++++--- tests/job_applications/tests.py | 16 ++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/itou/job_applications/models.py b/itou/job_applications/models.py index 0d8640dbc6..f4671a3b89 100644 --- a/itou/job_applications/models.py +++ b/itou/job_applications/models.py @@ -17,6 +17,7 @@ from itou.companies.models import CompanyMembership from itou.eligibility.enums import AuthorKind from itou.eligibility.models import EligibilityDiagnosis, SelectedAdministrativeCriteria +from itou.gps.models import FollowUpGroup from itou.job_applications import notifications as job_application_notifications from itou.job_applications.enums import ( ARCHIVABLE_JOB_APPLICATION_STATES_MANUAL, @@ -1011,6 +1012,9 @@ def accept(self, *, user): self.approval_delivery_mode = self.APPROVAL_DELIVERY_MODE_AUTOMATIC self.approval.unsuspend(self.hiring_start_at) + # Sync GPS groups + FollowUpGroup.objects.follow_beneficiary(self.job_seeker, user) + @xwf_models.transition() def postpone(self, *, user): # Send notification. diff --git a/tests/gps/test_management_commands.py b/tests/gps/test_management_commands.py index 288a102c83..fb3ff55fff 100644 --- a/tests/gps/test_management_commands.py +++ b/tests/gps/test_management_commands.py @@ -14,6 +14,7 @@ from itou.companies.enums import CompanyKind from itou.gps.models import FollowUpGroup, FollowUpGroupMembership, FranceTravailContact from itou.job_applications.enums import JobApplicationState +from itou.job_applications.models import JobApplicationTransitionLog from itou.users.models import JobSeekerProfile from tests.companies.factories import CompanyWith4MembershipsFactory from tests.eligibility.factories import GEIQEligibilityDiagnosisFactory, IAEEligibilityDiagnosisFactory @@ -86,7 +87,12 @@ def test_group_creation(self): ) # Needed to create the transition log entries user_who_accepted = job_application_with_approval.to_company.members.last() - job_application_with_approval.accept(user=user_who_accepted) + # Don't call accept that now creates a group and membership automatically + JobApplicationTransitionLog.objects.create( + job_application=job_application_with_approval, + to_state=JobApplicationState.ACCEPTED, + user=user_who_accepted, + ) should_be_created_groups_counter += 1 should_be_created_memberships += 3 # employer who accepted, employer who made the diagnosis and prescriber @@ -172,7 +178,12 @@ def test_job_application_accepted(self): state=JobApplicationState.PROCESSING, ) user = job_application_accepted.to_company.members.first() - job_application_accepted.accept(user=user) + # Don't call accept that now creates a group and membership automatically + JobApplicationTransitionLog.objects.create( + job_application=job_application_accepted, + to_state=JobApplicationState.ACCEPTED, + user=user, + ) assert FollowUpGroup.objects.count() == 0 assert FollowUpGroupMembership.objects.count() == 0 @@ -341,7 +352,12 @@ def test_group_create_at_update(self): # >>> Employer membership creation date employer_membership_date = timezone.now() # the employer accepts the last job app - accepted_job_app.accept(user=employer) + # Don't call accept that now creates a group and membership automatically + JobApplicationTransitionLog.objects.create( + job_application=accepted_job_app, + to_state=JobApplicationState.ACCEPTED, + user=employer, + ) with freeze_time("2022-06-01 00:00:08"): # Another iae diag from the employer IAEEligibilityDiagnosisFactory(job_seeker=job_seeker, from_employer=True, author=employer) diff --git a/tests/job_applications/tests.py b/tests/job_applications/tests.py index 513d0a4f65..6703262ab7 100644 --- a/tests/job_applications/tests.py +++ b/tests/job_applications/tests.py @@ -17,6 +17,7 @@ from itou.eligibility.enums import AdministrativeCriteriaLevel from itou.eligibility.models import AdministrativeCriteria, EligibilityDiagnosis from itou.employee_record.enums import Status +from itou.gps.models import FollowUpGroup, FollowUpGroupMembership from itou.job_applications.admin_forms import JobApplicationAdminForm from itou.job_applications.enums import ( JobApplicationState, @@ -165,6 +166,21 @@ def test_inverted_vae_contract(self): JobApplicationFactory(to_company__kind=CompanyKind.AI, inverted_vae_contract=True).clean() assert "Un contrat associé à une VAE inversée n'est possible que pour les GEIQ" in str(excinfo.value) + def test_accept_follow_up_group(self): + job_application = JobApplicationFactory( + sent_by_authorized_prescriber_organisation=True, + state=JobApplicationState.PROCESSING, + ) + user = job_application.to_company.members.first() + assert not FollowUpGroup.objects.exists() + + job_application.accept(user=user) + group = FollowUpGroup.objects.get() + assert group.beneficiary == job_application.job_seeker + membership = FollowUpGroupMembership.objects.get(follow_up_group=group) + assert membership.member == user + assert membership.creator == user + class TestJobApplicationQueryset: def test_is_active_company_member(self):