PASS IAE: La date de début du contrat doit être avant la date de fin du PASS [GEN-2225]#5248
Conversation
tonial
left a comment
There was a problem hiding this comment.
Je rejoins le commentaire de @xavfernandez sur le make_date_timezone_aware.
Pour le reste, par contre, c'est top 👍
rsebille
left a comment
There was a problem hiding this comment.
Désolé je fait l'hélicoptère mais vu qu'on en a discuté (longuement) en atelier métier bah j'ai dû y mettre mon nez plus que je le voulais et le changement me semble bizarre par rapport au problème de base.
On se retrouve à faire des modifications profondes au niveau de la logique de validité des diagnostiques et des PASS IAE (et donc de si on en délivre un nouveau ou pas, toussa toussa) alors que le problème c'est de bloquer les employeurs d'accepter une candidature avec une date de début après la date de fin d'un PASS IAE et qui j'ai l'impression (mais je suis peut-être naif) pourrais se régler avec quelques lignes dans JobApplication.accept() :
if self.hiring_start_at > self.approval.end_at:
raise xwf_models.AbortTransition(...)et de faire grosso modo la même chose dans le formulaire pour afficher une erreur à l'utilisateur avant même d'arriver dans JobApplication.accept()
d48a7e0 to
0ef8cd8
Compare
|
@rsebille j'ai implementé ta solution, sont ces changements ceux que t'avais en tête ? J'aime que ça impacte moins le code. J'ai eu besoin d'enlever ce test que j'avais fais pour la filtration des PASS IAEs expirés le moment d'embauche : def test_list_for_siae_filtered_by_eligibility_validated(self, client):
company = CompanyFactory(with_membership=True)
employer = company.members.first()
job_app = JobApplicationFactory(to_company=company, eligibility_diagnosis=None)
_another_job_app = JobApplicationFactory(to_company=company, eligibility_diagnosis=None)
client.force_login(employer)
response = client.get(reverse("apply:list_for_siae"), {"eligibility_validated": True})
assert response.context["job_applications_page"].object_list == []
# Authorized prescriber diagnosis
diagnosis = IAEEligibilityDiagnosisFactory(from_prescriber=True, job_seeker=job_app.job_seeker)
response = client.get(reverse("apply:list_for_siae"), {"eligibility_validated": True})
assert response.context["job_applications_page"].object_list == [job_app]
# Make sure the diagnostic expired - it should be ignored
diagnosis.expires_at = timezone.localdate() - datetime.timedelta(
days=diagnosis.EXPIRATION_DELAY_MONTHS * 31 + 1
)
diagnosis.save(update_fields=("expires_at",))
response = client.get(reverse("apply:list_for_siae"), {"eligibility_validated": True})
assert response.context["job_applications_page"].object_list == []
# Not expired, but will be when the job starts
diagnosis.expires_at = timezone.localdate() + datetime.timedelta(days=1)
diagnosis.save(update_fields=("expires_at",))
job_app.hiring_start_at = timezone.localdate() + datetime.timedelta(days=2)
job_app.save(update_fields=("hiring_start_at",))
response = client.get(reverse("apply:list_for_siae"), {"eligibility_validated": True})
assert response.context["job_applications_page"].object_list == []Mais vu que le début d'embauche est quelque chose qui peut changer, c'est peut-être le comportement attendu en fait ? |
rsebille
left a comment
There was a problem hiding this comment.
👍, c'est quand même moins menaçant maintenant que avant :D.
2-3 trucs au niveau des tests, mais c'est de la forme donc tu vois ce que tu prend ou pas.
Mais vu que le début d'embauche est quelque chose qui peut changer, c'est peut-être le comportement attendu en fait ?
Oui, ça me choque pas que le filtre teste l'éligibilité actuelle et pas futur. Mais tu peux confirmer avec le métier pour être sûr.
| # jobseeker has a PASS IAE, but it ends before the start date of the job | ||
| assert EligibilityDiagnosis.objects.filter(job_seeker=self.job_seeker).count() == 1 | ||
| expired_date = timezone.now() + datetime.timedelta(days=1) | ||
| EligibilityDiagnosis.objects.filter(job_seeker=self.job_seeker).update(expires_at=expired_date) | ||
| self.job_seeker.approvals.update(end_at=expired_date) | ||
| assert job_application.hiring_start_at > self.job_seeker.latest_approval.end_at |
There was a problem hiding this comment.
J'ai la forte impression que tout ceci devrais être fait directement au niveau de l'appel à .create_job_application() et pas en plusieurs étapes avec des vérifications au milieu, tout l'intérêt de pouvoir faire du déclaratif comme nous le permettent les factory est justement de donner un état de départ connu et fixe.
There was a problem hiding this comment.
Assez simple 🙂 self.create_job_application(..., approval=ApprovalFactory(end_at=(timezone.now() + datetime.timedelta(days=1))))
J'aime tester les assumptions dans mes tests mais ce n'est pas un avis fort (comme par example assert job_application.hiring_start_at > self.job_seeker.latest_approval.end_at)
| # employer amends the situation by submitting a different hiring start date | ||
| post_data["hiring_start_at"] = timezone.localdate().strftime(DuetDatePickerWidget.INPUT_DATE_FORMAT) | ||
| response, _ = self.accept_job_application(client, job_application, post_data=post_data, assert_successful=True) | ||
|
|
There was a problem hiding this comment.
Pareil, ce comportement est très certainement déjà testé ailleurs, et sûrement en long et en travers.
Les tests sont aussi du code donc faut le traiter de la même manière, et avoir une fonction pas trop longue et qui ne fait qu'une chose c'est toujours mieux, surtout quand on revient dessus des mois après ;).
There was a problem hiding this comment.
Je veux garder celui-ci je pense, les assertions sont reutilisées de accept_job_application et je pense que c'est un plus fort test quand il preuve que l'employeur peut rectifier la situation en changeant le hiring_start_date
There was a problem hiding this comment.
Je trouve également ce test pertinent. Dommage d’avoir des helpers «compliqués» comme accept_job_application et _accept_view_post_data, mais ce n’est que l’état actuel des choses, rien de nouveau avec cette PR.
0ef8cd8 to
b1ce34d
Compare
|
🥁 La recette jetable est prête ! 👉 Je veux tester cette PR ! |
b1ce34d to
b35fb42
Compare
francoisfreitag
left a comment
There was a problem hiding this comment.
Ça m’a l’air très bien. Je ferai quelques tests dans le navigateur demain pour m’assurer que tout fonctionne, et après ces quelques commentaires, on devrait être proches de pouvoir corriger ce bug. Merci !
| "hiring_end_at": "", | ||
| "pole_emploi_id": self.job_seeker.jobseeker_profile.pole_emploi_id, | ||
| "lack_of_pole_emploi_id_reason": self.job_seeker.jobseeker_profile.lack_of_pole_emploi_id_reason, | ||
| "birthdate": self.job_seeker.jobseeker_profile.birthdate.isoformat(), |
There was a problem hiding this comment.
Je taquine, mais lui n’a pas droit à .strftime(DuetDatePickerWidget.INPUT_DATE_FORMAT)?
(peu importe, Django accepte bien les deux)
There was a problem hiding this comment.
Petit gagne d'uniformité quand même 👍
Copié-collé des autres tests du fichier, je les ai tous reglé dans un commit séparé
b35fb42 to
620cace
Compare
| not self.instance.hiring_without_approval | ||
| and self.company.is_subject_to_eligibility_rules | ||
| and self.job_seeker.has_valid_approval | ||
| and hiring_start_at > self.job_seeker.latest_approval.end_at |
There was a problem hiding this comment.
J'ai tentativement décidé ignorer la présence du champ JobApplication.approval. Je ne pense pas que son expiration sera jamais différent que l'approbation la plus récente sur le candidat ?
There was a problem hiding this comment.
les-emplois/itou/job_applications/models.py
Line 994 in e8fb8e5
C’est en effet la même valeur que latest_approval. On ne peut effectivement pas utiliser JobApplication.approval ici, car la valeur est définie dans la transition accept qui sera appelée après que le formulaire soit instancié et validé.
620cace to
311223f
Compare
311223f to
1501525
Compare
francoisfreitag
left a comment
There was a problem hiding this comment.
L’ensemble me semble bien 👍
Les commits ne passent pas les tests individuellement. Le premier commit a une erreur :
____________________________________________________ TestProcessAcceptViews.test_accept_hiring_date_after_approval _____________________________________________________
[gw8] linux -- Python 3.13.2 /home/freitafr/dev/itou-review/.venv/bin/python3
self = <tests.www.apply.test_process.TestProcessAcceptViews object at 0x7e32045eac50>, client = <tests.utils.test.ItouClient object at 0x7e320358f890>
mocker = <pytest_mock.plugin.MockerFixture object at 0x7e32036ed400>
def test_accept_hiring_date_after_approval(self, client, mocker):
# jobseeker has a PASS IAE, but it ends before the start date of the job
job_application = self.create_job_application(
job_seeker=self.job_seeker,
to_company=self.company,
sent_by_authorized_prescriber_organisation=True,
> approval=ApprovalFactory(end_at=(timezone.now() + datetime.timedelta(days=1))),
hiring_start_at=timezone.localdate() + datetime.timedelta(days=2),
)
tests/www/apply/test_process.py:2218:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv/lib/python3.13/site-packages/factory/base.py:43: in __call__
return cls.create(**kwargs)
.venv/lib/python3.13/site-packages/factory/base.py:539: in create
return cls._generate(enums.CREATE_STRATEGY, kwargs)
.venv/lib/python3.13/site-packages/factory/django.py:122: in _generate
return super()._generate(strategy, params)
.venv/lib/python3.13/site-packages/factory/base.py:468: in _generate
return step.build()
.venv/lib/python3.13/site-packages/factory/builder.py:274: in build
instance = self.factory_meta.instantiate(
.venv/lib/python3.13/site-packages/factory/base.py:320: in instantiate
return self.factory._create(model, *args, **kwargs)
tests/utils/factory_boy.py:12: in _create
return super()._create(model_class, *args, **kwargs)
.venv/lib/python3.13/site-packages/factory/django.py:175: in _create
return manager.create(*args, **kwargs)
.venv/lib/python3.13/site-packages/django/db/models/manager.py:87: in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
.venv/lib/python3.13/site-packages/django/db/models/query.py:679: in create
obj.save(force_insert=True, using=self.db)
itou/approvals/models.py:648: in save
super().save(*args, **kwargs)
.venv/lib/python3.13/site-packages/django/db/models/base.py:892: in save
self.save_base(
.venv/lib/python3.13/site-packages/django/db/models/base.py:998: in save_base
updated = self._save_table(
.venv/lib/python3.13/site-packages/django/db/models/base.py:1161: in _save_table
results = self._do_insert(
.venv/lib/python3.13/site-packages/django/db/models/base.py:1202: in _do_insert
return manager._insert(
.venv/lib/python3.13/site-packages/django/db/models/manager.py:87: in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
.venv/lib/python3.13/site-packages/django/db/models/query.py:1847: in _insert
return query.get_compiler(using=using).execute_sql(returning_fields)
.venv/lib/python3.13/site-packages/django/db/models/sql/compiler.py:1835: in execute_sql
for sql, params in self.as_sql():
.venv/lib/python3.13/site-packages/django/db/models/sql/compiler.py:1760: in as_sql
self.prepare_value(field, self.pre_save_val(field, obj))
.venv/lib/python3.13/site-packages/django/db/models/sql/compiler.py:1708: in pre_save_val
return field.pre_save(obj, add=True)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <django.db.models.fields.DateField: end_at>, model_instance = <Approval: XXXXX5565710>, add = True
def strict_pre_save(self, model_instance, add):
# DateTimeField inherits DateField, hence the check on self.__class__
if self.__class__ is DateField and isinstance(getattr(model_instance, self.attname), datetime.datetime):
> raise ValueError(
f"<{model_instance}>.{self.attname}={getattr(model_instance, self.attname)} needs to be a date"
)
E ValueError: <XXXXX5565710>.end_at=2025-03-11 10:18:27.247316+00:00 needs to be a date
tests/conftest.py:675: ValueError
======================================================================= short test summary info ========================================================================
FAILED tests/www/apply/test_process.py::TestProcessAcceptViews::test_accept_hiring_date_after_approval - ValueError: <XXXXX5565710>.end_at=2025-03-11 10:18:27.247316+00:00 needs to be a date
| hiring_start_at=timezone.localdate() + relativedelta(days=2), | ||
| approval__end_at=timezone.localdate() + relativedelta(days=1), |
There was a problem hiding this comment.
On pourrait stocker et réutiliser today = timezone.localdate() pour éviter un petit raté vers minuit. 🤷
There was a problem hiding this comment.
J'ai ajouté un commit pour cibler les autres tests dans test_process.py : 1f5c984
1501525 to
1f5c984
Compare
3c49a54 to
271e80f
Compare
Adding this improves the validation of hiring_start_at (with regards to the job seeker's PASS IAE), for hiring declarations made by employers
Tweaks a number of instances in test_process.py where localdate() is called twice in two lines. If ran at midnight exactly it's possible the test will run with bad dates
271e80f to
a06acf8
Compare
🤔 Pourquoi ?
En ce moment c'est possible d'embaucher un candidat après son PASS IAE est expiré.
🍰 Comment ?
AcceptForm).JobApplication.acceptqui empêche qu'une candidature dans cette situation serait acceptée.🚨 À vérifier
🏝️ Comment tester
💻 Captures d'écran