Skip to content

Commit

Permalink
feat(apply): third-party JobSeeker creation includes birthplace fields
Browse files Browse the repository at this point in the history
refactor(BirthPlaceAndCountryMixin): added comments

fix: test

refactor(BirthPlaceAndCountryMixin): reverse_lazy on autocomplete url fetch

feat(modify jobseeker): using BirthPlaceAndCountryMixin

refactor(BirthPlaceAndCountryMixin): avoid manipulation of PROFILE_FIELDS during __init__

feat(Country): cache france_id

minor performance improvement for tests and for loading pages - but principally included to fix a flaky test

refactor: solution reusability

tests.www.apply: Use `assertSnapshotQueries()` in `test_submit.py`

fix: snapshot

requested changes

added birth fields to profile_infos.html
  • Loading branch information
calummackervoy committed Sep 25, 2024
1 parent 73a62df commit 0500419
Show file tree
Hide file tree
Showing 13 changed files with 329 additions and 74 deletions.
23 changes: 8 additions & 15 deletions itou/asp/forms.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django import forms
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.urls import reverse_lazy
from django.utils.functional import SimpleLazyObject

from itou.asp.models import Commune, Country
from itou.utils.validators import validate_birth_location
from itou.utils.widgets import RemoteAutocompleteSelect2Widget


Expand All @@ -21,13 +22,13 @@ class BirthPlaceAndCountryMixin(forms.ModelForm):
required=False,
widget=RemoteAutocompleteSelect2Widget(
attrs={
"data-ajax--url": SimpleLazyObject(lambda: reverse("autocomplete:communes")),
"data-ajax--url": reverse_lazy("autocomplete:communes"),
"data-ajax--cache": "true",
"data-ajax--type": "GET",
"data-minimum-input-length": 1,
"data-placeholder": "Nom de la commune",
"data-disable-target": "#id_birth_country",
"data-target-value": SimpleLazyObject(lambda: f"{Country.objects.get(code=Country._CODE_FRANCE).pk}"),
"data-target-value": SimpleLazyObject(lambda: f"{Country.france_id}"),
}
),
)
Expand Down Expand Up @@ -58,7 +59,7 @@ def clean(self):
# That's also why we can't make it mandatory.
# See utils.js > toggleDisableAndSetValue
if birth_place:
self.cleaned_data["birth_country"] = Country.objects.get(code=Country._CODE_FRANCE)
self.cleaned_data["birth_country"] = Country.objects.get(code=Country.INSEE_CODE_FRANCE)
else:
# Display the error above the field instead of top of page.
self.add_error("birth_country", "Le pays de naissance est obligatoire.")
Expand All @@ -77,17 +78,9 @@ def clean(self):
)
self.add_error("birth_place", msg)

def treat_birth_fields(self):
# class targets User and JobSeekerProfile models
jobseeker_profile = self.instance.jobseeker_profile if hasattr(self.instance, "jobseeker_profile") else self.instance

def _post_clean(self):
super()._post_clean()
try:
jobseeker_profile.birth_place = self.cleaned_data.get("birth_place")
jobseeker_profile.birth_country = self.cleaned_data.get("birth_country")
jobseeker_profile._clean_birth_fields()
validate_birth_location(self.cleaned_data.get("birth_country"), self.cleaned_data.get("birth_place"))
except ValidationError as e:
self._update_errors(e)

def _post_clean(self):
super()._post_clean()
self.treat_birth_fields()
11 changes: 9 additions & 2 deletions itou/asp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.functional import cached_property, classproperty
from unidecode import unidecode

from itou.utils.models import DateRange
Expand Down Expand Up @@ -463,7 +463,8 @@ class Country(PrettyPrintMixin, models.Model):
Imported from ASP reference file: ref_grp_pays_v1, ref_insee_pays_v4.csv
"""

_CODE_FRANCE = "100"
INSEE_CODE_FRANCE = "100"
_ID_FRANCE = None

class Group(models.TextChoices):
FRANCE = "1", "France"
Expand All @@ -485,6 +486,12 @@ class Meta:
verbose_name_plural = "pays"
ordering = ["name"]

@classproperty
def france_id(cls):
if cls._ID_FRANCE is None:
cls._ID_FRANCE = Country.objects.get(code=Country.INSEE_CODE_FRANCE).pk
return cls._ID_FRANCE


class SiaeMeasure(models.TextChoices):
"""
Expand Down
10 changes: 10 additions & 0 deletions itou/templates/apply/includes/profile_infos.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ <h2>État civil</h2>
<li class="mb-3">
Date de naissance : <b>le {{ profile.birthdate }}</b>
</li>
{% if profile.birth_place %}
<li class="mb-3">
Commune de naissance : <b>{{ profile.birth_place.name }} {{ profile.birth_place.department_code }}</b>
</li>
{% endif %}
{% if profile.birth_country %}
<li class="mb-3">
Pays de naissance : <b>{{ profile.birth_country.name }}</b>
</li>
{% endif %}
</ul>
<hr class="my-4">

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
{% bootstrap_field form.lack_of_nir %}
{% bootstrap_field form.lack_of_nir_reason wrapper_class=form.lack_of_nir_reason.field.form_group_class %}
{% bootstrap_field form.birthdate %} {# TODO: Duet border-color button is not of the correct color #}
{% if form.birth_place %}
{% bootstrap_field form.birth_place %}
{% bootstrap_field form.birth_country %}
{% endif %}
</fieldset>

{% if confirmation_needed %}
Expand Down
3 changes: 2 additions & 1 deletion itou/users/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def __init__(self, *args, instance=None, **kwargs):
initial = kwargs.pop("initial", {})
super().__init__(*args, instance=instance, initial=profile_initial | initial, **kwargs)
for field in self.PROFILE_FIELDS:
self.fields[field] = JobSeekerProfile._meta.get_field(field).formfield()
if field not in self.fields:
self.fields[field] = JobSeekerProfile._meta.get_field(field).formfield()

def save(self, commit=True):
user = super().save(commit=commit)
Expand Down
18 changes: 2 additions & 16 deletions itou/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from itou.asp.models import (
AllocationDuration,
Commune,
Country,
EducationLevel,
LaneExtension,
LaneType,
Expand All @@ -35,7 +34,7 @@
from itou.utils.db import or_queries
from itou.utils.models import UniqueConstraintWithErrorCode
from itou.utils.templatetags.str_filters import mask_unless
from itou.utils.validators import validate_birthdate, validate_nir, validate_pole_emploi_id
from itou.utils.validators import validate_birth_location, validate_birthdate, validate_nir, validate_pole_emploi_id

from .enums import IdentityProvider, LackOfNIRReason, LackOfPoleEmploiId, Title, UserKind

Expand Down Expand Up @@ -772,17 +771,10 @@ class JobSeekerProfile(models.Model):
with Hexa norms (but compliant enough to be accepted by ASP backend).
"""

# Used for validation of birth country / place
INSEE_CODE_FRANCE = Country._CODE_FRANCE

ERROR_NOT_RESOURCELESS_IF_OETH_OR_RQTH = "La personne n'est pas considérée comme sans ressources si OETH ou RQTH"
ERROR_UNEMPLOYED_BUT_RQTH_OR_OETH = (
"La personne ne peut être considérée comme sans emploi si employée OETH ou RQTH"
)
ERROR_MUST_PROVIDE_BIRTH_PLACE = "Si le pays de naissance est la France, la commune de naissance est obligatoire"
ERROR_BIRTH_COMMUNE_WITH_FOREIGN_COUNTRY = (
"Il n'est pas possible de saisir une commune de naissance hors de France"
)

ERROR_HEXA_LANE_TYPE = "Le type de voie est obligatoire"
ERROR_HEXA_LANE_NAME = "Le nom de voie est obligatoire"
Expand Down Expand Up @@ -1121,13 +1113,7 @@ def _clean_birth_fields(self):
Mainly coherence checks for birth country / place.
Must be non blocking if these fields are not provided.
"""
# If birth country is France, then birth place must be provided
if self.birth_country and self.birth_country.code == self.INSEE_CODE_FRANCE and not self.birth_place:
raise ValidationError(self.ERROR_MUST_PROVIDE_BIRTH_PLACE)

# If birth country is not France, do not fill a birth place (no ref file)
if self.birth_country and self.birth_country.code != self.INSEE_CODE_FRANCE and self.birth_place:
raise ValidationError(self.ERROR_BIRTH_COMMUNE_WITH_FOREIGN_COUNTRY)
validate_birth_location(self.birth_country, self.birth_place)

# This used to be the `clean` method for the global model validation
# when using forms.
Expand Down
12 changes: 12 additions & 0 deletions itou/utils/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
from django.utils import timezone

from itou.asp.models import Country


alphanumeric = RegexValidator(r"^[0-9a-zA-Z]*$", "Seuls les caractères alphanumériques sont autorisés.")

Expand Down Expand Up @@ -90,6 +92,16 @@ def validate_birthdate(birthdate):
raise ValidationError("La personne doit avoir plus de 16 ans.")


def validate_birth_location(birth_country, birth_place):
# If birth country is France, then birth place must be provided
if birth_country and birth_country.code == Country.INSEE_CODE_FRANCE and not birth_place:
raise ValidationError("Il n'est pas possible de saisir une commune de naissance hors de France.")

# If birth country is not France, do not fill a birth place (no ref file)
if birth_country and birth_country.code != Country.INSEE_CODE_FRANCE and birth_place:
raise ValidationError("Si le pays de naissance est la France, la commune de naissance est obligatoire.")


AF_NUMBER_PREFIX_REGEXPS = [
r"^ACI\d{2}[A-Z\d]\d{6}$",
r"^EI\d{2}[A-Z\d]\d{6}$",
Expand Down
12 changes: 4 additions & 8 deletions itou/www/apply/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,17 @@ def clean(self):
JobSeekerProfile.clean_pole_emploi_fields(self.cleaned_data)


class CreateOrUpdateJobSeekerStep1Form(JobSeekerNIRUpdateMixin, JobSeekerProfileFieldsMixin, forms.ModelForm):
class CreateOrUpdateJobSeekerStep1Form(
JobSeekerNIRUpdateMixin, JobSeekerProfileFieldsMixin, BirthPlaceAndCountryMixin, forms.ModelForm
):
REQUIRED_FIELDS = [
"title",
"first_name",
"last_name",
"birthdate",
]

PROFILE_FIELDS = ["birthdate", "nir", "lack_of_nir_reason"]
PROFILE_FIELDS = ["birth_country", "birthdate", "birth_place", "nir", "lack_of_nir_reason"]

class Meta:
model = User
Expand All @@ -194,12 +196,6 @@ def __init__(self, *args, **kwargs):
)


class CreateOrUpdateJobSeekerWithBirthPlaceAndCountryStep1Form(
BirthPlaceAndCountryMixin, CreateOrUpdateJobSeekerStep1Form
):
pass


class CreateOrUpdateJobSeekerStep2Form(JobSeekerAddressForm, forms.ModelForm):
class Meta(JobSeekerAddressForm.Meta):
fields = JobSeekerAddressForm.Meta.fields + ["phone"]
Expand Down
43 changes: 33 additions & 10 deletions itou/www/apply/views/submit_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,14 +532,18 @@ def setup(self, request, *args, **kwargs):
) # TODO(xfernandez): drop fallback on self.job_seeker_session["user"] in a week
session_nir = self.job_seeker_session.get("profile", {}).get("nir")
session_lack_of_nir_reason = self.job_seeker_session.get("profile", {}).get("lack_of_nir_reason")
session_birth_place = self.job_seeker_session.get("profile", {}).get("birth_place")
session_birth_country = self.job_seeker_session.get("profile", {}).get("birth_country")

self.form = CreateOrUpdateJobSeekerStep1Form(
data=request.POST or None,
initial=self.job_seeker_session.get("user", {})
| {
"birthdate": session_birthdate if session_birthdate is not None else None,
"nir": session_nir if session_nir is not None else None,
"lack_of_nir_reason": session_lack_of_nir_reason if session_lack_of_nir_reason is not None else None,
"birthdate": session_birthdate,
"nir": session_nir,
"lack_of_nir_reason": session_lack_of_nir_reason,
"birth_place": session_birth_place,
"birth_country": session_birth_country,
},
)

Expand Down Expand Up @@ -675,22 +679,30 @@ def _get_user_data_from_session(self):
}

def _get_profile_data_from_session(self):
# Dummy fields used by CreateOrUpdateJobSeekerStep3Form()
fields_to_exclude = [
# Dummy fields used by CreateOrUpdateJobSeekerStep3Form()
"pole_emploi",
"pole_emploi_id_forgotten",
"rsa_allocation",
"unemployed",
"ass_allocation",
"aah_allocation",
"lack_of_nir",
# ForeignKeys - the session value will be the ID serialization and not the instance
"birth_place",
"birth_country",
]

birth_data = {
"birth_place_id": self.job_seeker_session.get("profile", {}).get("birth_place"),
"birth_country_id": self.job_seeker_session.get("profile", {}).get("birth_country"),
}

# TODO(xfernandez): remove user session birthdate handling in a week
if birthdate_from_user_session := self.job_seeker_session.get("user", {}).get("birthdate"):
user_data = {"birthdate": birthdate_from_user_session}
else:
user_data = {}
return user_data | {
birth_data |= {"birthdate": birthdate_from_user_session}

return birth_data | {
k: v for k, v in self.job_seeker_session.get("profile").items() if k not in fields_to_exclude
}

Expand Down Expand Up @@ -1523,17 +1535,28 @@ def __init__(self):
self.updated_user_fields = []

def _get_profile_data_from_session(self):
# Dummy fields used by CreateOrUpdateJobSeekerStep3Form()
fields_to_exclude = [
# Dummy fields used by CreateOrUpdateJobSeekerStep3Form()
"pole_emploi",
"pole_emploi_id_forgotten",
"rsa_allocation",
"unemployed",
"ass_allocation",
"aah_allocation",
"lack_of_nir",
# ForeignKeys - the session value will be the ID serialization and not the instance
"birth_place",
"birth_country",
]
return {k: v for k, v in self.job_seeker_session.get("profile", {}).items() if k not in fields_to_exclude}

birth_data = {
"birth_place_id": self.job_seeker_session.get("profile", {}).get("birth_place"),
"birth_country_id": self.job_seeker_session.get("profile", {}).get("birth_country"),
}

return birth_data | {
k: v for k, v in self.job_seeker_session.get("profile", {}).items() if k not in fields_to_exclude
}

def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion itou/www/employee_record_views/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class NewEmployeeRecordStep1Form(BirthPlaceAndCountryMixin, JobSeekerProfileFiel
- birth place and birth country of the employee
"""

PROFILE_FIELDS = ["birthdate"]
PROFILE_FIELDS = ["birth_country", "birthdate", "birth_place"]
READ_ONLY_FIELDS = []
REQUIRED_FIELDS = [
"title",
Expand Down
13 changes: 10 additions & 3 deletions tests/gps/test_create_beneficiary.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
from django.utils import timezone
from pytest_django.asserts import assertContains, assertRedirects

from itou.asp.models import RSAAllocation
from itou.asp.models import Commune, Country, RSAAllocation
from itou.companies import enums as companies_enums
from itou.companies.models import Company
from itou.siae_evaluations.models import Sanctions
from itou.users.enums import LackOfPoleEmploiId, UserKind
from itou.users.models import User
from itou.utils.mocks.address_format import mock_get_geocoding_data_by_ban_api_resolved
from itou.utils.models import InclusiveDateRange
from tests.cities.factories import create_test_cities
from tests.cities.factories import create_city_geispolsheim, create_test_cities
from tests.companies.factories import CompanyWithMembershipAndJobsFactory
from tests.prescribers.factories import PrescriberOrganizationWithMembershipFactory
from tests.siae_evaluations.factories import EvaluatedSiaeFactory
Expand Down Expand Up @@ -109,17 +109,24 @@ def test_create_job_seeker(_mock, client):
),
)

geispolsheim = create_city_geispolsheim()
birthdate = dummy_job_seeker.jobseeker_profile.birthdate

post_data = {
"title": dummy_job_seeker.title,
"first_name": dummy_job_seeker.first_name,
"last_name": dummy_job_seeker.last_name,
"birthdate": dummy_job_seeker.jobseeker_profile.birthdate,
"birthdate": birthdate,
"lack_of_nir": False,
"lack_of_nir_reason": "",
"birth_place": Commune.objects.by_insee_code_and_period(geispolsheim.code_insee, birthdate).id,
"birth_country": Country.france_id,
}
response = client.post(next_url, data=post_data)
expected_job_seeker_session["profile"]["birthdate"] = post_data.pop("birthdate")
expected_job_seeker_session["profile"]["lack_of_nir_reason"] = post_data.pop("lack_of_nir_reason")
expected_job_seeker_session["profile"]["birth_place"] = post_data.pop("birth_place")
expected_job_seeker_session["profile"]["birth_country"] = post_data.pop("birth_country")
expected_job_seeker_session["user"] |= post_data
assert client.session[job_seeker_session_name] == expected_job_seeker_session

Expand Down
Loading

0 comments on commit 0500419

Please sign in to comment.