From 50b5e6e83cd8085426da6a4239c31f89714419fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=CC=81o=20S?= Date: Thu, 1 Aug 2024 11:29:55 +0200 Subject: [PATCH] feat(rdvi): add RDV-I appointment details for companies --- itou/rdv_insertion/api.py | 20 + itou/rdv_insertion/enums.py | 39 ++ itou/rdv_insertion/migrations/0001_initial.py | 224 +++++- itou/rdv_insertion/models.py | 143 +++- .../apply/includes/appointments.html | 91 +++ .../buttons/rdv_insertion_invite.html | 18 +- .../apply/includes/invitation_requests.html | 30 + .../includes/list_card_body_company.html | 7 + .../apply/includes/list_job_applications.html | 2 +- .../apply/process_details_company.html | 25 + itou/utils/mocks/rdv_insertion.py | 1 + itou/utils/templatetags/datetime_filters.py | 23 + itou/www/apply/urls.py | 6 + itou/www/apply/views/list_views.py | 25 +- itou/www/apply/views/process_views.py | 117 +++- tests/rdv_insertion/factories.py | 122 +++- tests/rdv_insertion/tests.py | 179 ++++- tests/users/test_admin.py | 2 + tests/utils/tests.py | 20 + .../test_detail_rdv_insertion.ambr | 660 ++++++++++++++++++ tests/www/apply/__snapshots__/test_list.ambr | 12 + .../test_list_rdv_insertion.ambr | 36 +- .../www/apply/__snapshots__/test_process.ambr | 505 +++++++------- tests/www/apply/test_detail_rdv_insertion.py | 324 +++++++++ tests/www/apply/test_list_rdv_insertion.py | 76 +- 25 files changed, 2398 insertions(+), 309 deletions(-) create mode 100644 itou/templates/apply/includes/appointments.html create mode 100644 itou/templates/apply/includes/invitation_requests.html create mode 100644 itou/utils/templatetags/datetime_filters.py create mode 100644 tests/www/apply/__snapshots__/test_detail_rdv_insertion.ambr create mode 100644 tests/www/apply/test_detail_rdv_insertion.py diff --git a/itou/rdv_insertion/api.py b/itou/rdv_insertion/api.py index e6c722af91e..175d2062ecf 100644 --- a/itou/rdv_insertion/api.py +++ b/itou/rdv_insertion/api.py @@ -1,3 +1,4 @@ +import logging from urllib.parse import urljoin import httpx @@ -5,9 +6,17 @@ from django.core.cache import cache from django.core.exceptions import ImproperlyConfigured +from .enums import InvitationStatus + + +logger = logging.getLogger(__name__) + RDV_S_CREDENTIALS_CACHE_KEY = "rdv-solidarites-credentials" +RDV_I_INVITATION_DELIVERED_STATUSES = ["delivered"] +RDV_I_INVITATION_NOT_DELIVERED_STATUSES = ["soft_bounce", "hard_bounce", "blocked", "invalid_email", "error"] + def get_api_credentials(refresh=False): """ @@ -37,3 +46,14 @@ def get_api_credentials(refresh=False): raise ImproperlyConfigured( "RDV-S settings must be set: RDV_SOLIDARITES_API_BASE_URL, RDV_SOLIDARITES_EMAIL, RDV_SOLIDARITES_PASSWORD" ) + + +def get_invitation_status(invitation_dict): + if invitation_dict.get("clicked"): + return InvitationStatus.OPENED + if delivery_status := invitation_dict.get("delivery_status"): + if delivery_status in RDV_I_INVITATION_DELIVERED_STATUSES: + return InvitationStatus.DELIVERED + if delivery_status in RDV_I_INVITATION_NOT_DELIVERED_STATUSES: + return InvitationStatus.NOT_DELIVERED + logger.error(f"Invalid RDV-I invitation status: '{delivery_status}' not in supported list") diff --git a/itou/rdv_insertion/enums.py b/itou/rdv_insertion/enums.py index 6f8f9519b15..d0f00447d2a 100644 --- a/itou/rdv_insertion/enums.py +++ b/itou/rdv_insertion/enums.py @@ -10,4 +10,43 @@ class InvitationType(models.TextChoices): class InvitationStatus(models.TextChoices): SENT = "sent", "Envoyée" DELIVERED = "delivered", "Délivrée" + NOT_DELIVERED = "not_delivered", "Non délivrée" OPENED = "opened", "Ouverte" + + +class ParticipationStatus(models.TextChoices): + UNKNOWN = "unknown", "Non déterminé" + SEEN = "seen", "RDV honoré" + EXCUSED = "excused", "RDV annulé à l’initiative de l’usager" + REVOKED = "revoked", "RDV annulé à l’initiative du service" + NOSHOW = "noshow", "Absence non excusée au RDV" + + +class InvitationRequestReasonCategory(models.TextChoices): + RSA_DROITS_DEVOIRS = "rsa_droits_devoirs", "RSA - droits et devoirs" + RSA_ORIENTATION = "rsa_orientation", "RSA orientation" + RSA_ORIENTATION_FRANCE_TRAVAIL = "rsa_orientation_france_travail", "RSA orientation France Travail" + RSA_ACCOMPAGNEMENT = "rsa_accompagnement", "RSA accompagnement" + RSA_ACCOMPAGNEMENT_SOCIAL = "rsa_accompagnement_social", "RSA accompagnement social" + RSA_ACCOMPAGNEMENT_SOCIOPRO = "rsa_accompagnement_sociopro", "RSA accompagnement socio-pro" + RSA_ORIENTATION_ON_PHONE_PLATFORM = ( + "rsa_orientation_on_phone_platform", + "RSA orientation sur plateforme téléphonique", + ) + RSA_CER_SIGNATURE = "rsa_cer_signature", "RSA signature CER" + RSA_INSERTION_OFFER = "rsa_insertion_offer", "RSA offre insertion pro" + RSA_FOLLOW_UP = "rsa_follow_up", "RSA suivi" + RSA_MAIN_TENDUE = "rsa_main_tendue", "RSA Main Tendue" + RSA_ATELIER_COLLECTIF_MANDATORY = "rsa_atelier_collectif_mandatory", "RSA Atelier collectif" + RSA_SPIE = "rsa_spie", "RSA SPIE" + RSA_INTEGRATION_INFORMATION = "rsa_integration_information", "RSA Information d'intégration" + RSA_ATELIER_COMPETENCES = "rsa_atelier_competences", "RSA Atelier compétences" + RSA_ATELIER_RENCONTRES_PRO = "rsa_atelier_rencontres_pro", "RSA Atelier rencontres professionnelles" + PSYCHOLOGUE = "psychologue", "Psychologue" + RSA_ORIENTATION_FREELANCE = "rsa_orientation_freelance", "RSA orientation - travailleurs indépendants" + RSA_ORIENTATION_COACHING = "rsa_orientation_coaching", "RSA orientation - coaching emploi" + ATELIER_ENFANTS_ADOS = "atelier_enfants_ados", "Atelier Enfants / Ados" + RSA_ORIENTATION_FILE_ACTIVE = "rsa_orientation_file_active", "RSA orientation file active" + SIAE_INTERVIEW = "siae_interview", "Entretien SIAE" + SIAE_COLLECTIVE_INFORMATION = "siae_collective_information", "Info coll. SIAE" + SIAE_FOLLOW_UP = "siae_follow_up", "Suivi SIAE" diff --git a/itou/rdv_insertion/migrations/0001_initial.py b/itou/rdv_insertion/migrations/0001_initial.py index f86a765ec36..81a11ce8a55 100644 --- a/itou/rdv_insertion/migrations/0001_initial.py +++ b/itou/rdv_insertion/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.6 on 2024-07-02 17:04 +# Generated by Django 5.0.7 on 2024-08-07 10:33 import uuid @@ -16,10 +16,69 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name="Location", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("name", models.CharField(editable=False, verbose_name="nom")), + ("address", models.CharField(editable=False, verbose_name="adresse")), + ("phone_number", models.CharField(editable=False, null=True, verbose_name="téléphone")), + ("rdv_insertion_id", models.IntegerField(editable=False, unique=True)), + ], + options={ + "verbose_name": "lieu d'un événement RDV-I", + "verbose_name_plural": "lieux d'événements RDV-I", + }, + ), + migrations.CreateModel( + name="WebhookEvent", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True, verbose_name="créée le")), + ("body", models.JSONField(editable=False)), + ("headers", models.JSONField(editable=False)), + ], + options={ + "verbose_name": "événement du webhook RDV-I", + "verbose_name_plural": "événements du webhook RDV-I", + }, + ), migrations.CreateModel( name="InvitationRequest", fields=[ ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ( + "reason_category", + models.CharField( + choices=[ + ("rsa_droits_devoirs", "RSA - droits et devoirs"), + ("rsa_orientation", "RSA orientation"), + ("rsa_orientation_france_travail", "RSA orientation France Travail"), + ("rsa_accompagnement", "RSA accompagnement"), + ("rsa_accompagnement_social", "RSA accompagnement social"), + ("rsa_accompagnement_sociopro", "RSA accompagnement socio-pro"), + ("rsa_orientation_on_phone_platform", "RSA orientation sur plateforme téléphonique"), + ("rsa_cer_signature", "RSA signature CER"), + ("rsa_insertion_offer", "RSA offre insertion pro"), + ("rsa_follow_up", "RSA suivi"), + ("rsa_main_tendue", "RSA Main Tendue"), + ("rsa_atelier_collectif_mandatory", "RSA Atelier collectif"), + ("rsa_spie", "RSA SPIE"), + ("rsa_integration_information", "RSA Information d'intégration"), + ("rsa_atelier_competences", "RSA Atelier compétences"), + ("rsa_atelier_rencontres_pro", "RSA Atelier rencontres professionnelles"), + ("psychologue", "Psychologue"), + ("rsa_orientation_freelance", "RSA orientation - travailleurs indépendants"), + ("rsa_orientation_coaching", "RSA orientation - coaching emploi"), + ("atelier_enfants_ados", "Atelier Enfants / Ados"), + ("rsa_orientation_file_active", "RSA orientation file active"), + ("siae_interview", "Entretien SIAE"), + ("siae_collective_information", "Info coll. SIAE"), + ("siae_follow_up", "Suivi SIAE"), + ], + verbose_name="catégorie de motif", + ), + ), ("created_at", models.DateTimeField(auto_now_add=True, verbose_name="créée le")), ("api_response", models.JSONField(editable=False)), ("rdv_insertion_user_id", models.IntegerField(db_index=True, editable=False)), @@ -61,12 +120,18 @@ class Migration(migrations.Migration): ( "status", models.CharField( - choices=[("sent", "Envoyée"), ("delivered", "Délivrée"), ("opened", "Ouverte")], + choices=[ + ("sent", "Envoyée"), + ("delivered", "Délivrée"), + ("not_delivered", "Non délivrée"), + ("opened", "Ouverte"), + ], default="sent", verbose_name="état", ), ), - ("rdv_insertion_invitation_id", models.IntegerField(editable=False, unique=True)), + ("delivered_at", models.DateTimeField(editable=False, null=True, verbose_name="délivrée le")), + ("rdv_insertion_id", models.IntegerField(editable=False, unique=True)), ( "invitation_request", models.ForeignKey( @@ -82,6 +147,159 @@ class Migration(migrations.Migration): "verbose_name_plural": "invitations RDV-I", }, ), + migrations.CreateModel( + name="Appointment", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ( + "status", + models.CharField( + choices=[ + ("unknown", "Non déterminé"), + ("seen", "RDV honoré"), + ("excused", "RDV annulé à l’initiative de l’usager"), + ("revoked", "RDV annulé à l’initiative du service"), + ("noshow", "Absence non excusée au RDV"), + ], + default="unknown", + editable=False, + verbose_name="état", + ), + ), + ( + "reason_category", + models.CharField( + choices=[ + ("rsa_droits_devoirs", "RSA - droits et devoirs"), + ("rsa_orientation", "RSA orientation"), + ("rsa_orientation_france_travail", "RSA orientation France Travail"), + ("rsa_accompagnement", "RSA accompagnement"), + ("rsa_accompagnement_social", "RSA accompagnement social"), + ("rsa_accompagnement_sociopro", "RSA accompagnement socio-pro"), + ("rsa_orientation_on_phone_platform", "RSA orientation sur plateforme téléphonique"), + ("rsa_cer_signature", "RSA signature CER"), + ("rsa_insertion_offer", "RSA offre insertion pro"), + ("rsa_follow_up", "RSA suivi"), + ("rsa_main_tendue", "RSA Main Tendue"), + ("rsa_atelier_collectif_mandatory", "RSA Atelier collectif"), + ("rsa_spie", "RSA SPIE"), + ("rsa_integration_information", "RSA Information d'intégration"), + ("rsa_atelier_competences", "RSA Atelier compétences"), + ("rsa_atelier_rencontres_pro", "RSA Atelier rencontres professionnelles"), + ("psychologue", "Psychologue"), + ("rsa_orientation_freelance", "RSA orientation - travailleurs indépendants"), + ("rsa_orientation_coaching", "RSA orientation - coaching emploi"), + ("atelier_enfants_ados", "Atelier Enfants / Ados"), + ("rsa_orientation_file_active", "RSA orientation file active"), + ("siae_interview", "Entretien SIAE"), + ("siae_collective_information", "Info coll. SIAE"), + ("siae_follow_up", "Suivi SIAE"), + ], + editable=False, + verbose_name="catégorie de motif", + ), + ), + ("reason", models.CharField(editable=False, verbose_name="motif")), + ("is_collective", models.BooleanField(editable=False, verbose_name="rendez-vous collectif")), + ("starts_at", models.DateTimeField(editable=False, verbose_name="commence le")), + ("duration", models.DurationField(editable=False, verbose_name="durée")), + ("canceled_at", models.DateTimeField(editable=False, null=True, verbose_name="annulé le")), + ("address", models.CharField(editable=False, verbose_name="adresse")), + ( + "total_participants", + models.PositiveSmallIntegerField(editable=False, null=True, verbose_name="nombre de participants"), + ), + ( + "max_participants", + models.PositiveSmallIntegerField( + editable=False, null=True, verbose_name="nombre max. de participants" + ), + ), + ("rdv_insertion_id", models.IntegerField(editable=False, unique=True)), + ( + "company", + models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.CASCADE, + related_name="rdvi_appointments", + to="companies.company", + verbose_name="entreprise", + ), + ), + ( + "location", + models.ForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="rdvi_appointments", + to="rdv_insertion.location", + verbose_name="lieu", + ), + ), + ], + options={ + "verbose_name": "rendez-vous RDV-I", + "verbose_name_plural": "rendez-vous RDV-I", + }, + ), + migrations.CreateModel( + name="Participation", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ( + "status", + models.CharField( + choices=[ + ("unknown", "Non déterminé"), + ("seen", "RDV honoré"), + ("excused", "RDV annulé à l’initiative de l’usager"), + ("revoked", "RDV annulé à l’initiative du service"), + ("noshow", "Absence non excusée au RDV"), + ], + default="unknown", + editable=False, + verbose_name="état", + ), + ), + ("rdv_insertion_id", models.IntegerField(editable=False, unique=True)), + ( + "appointment", + models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.CASCADE, + related_name="rdvi_participations", + to="rdv_insertion.appointment", + verbose_name="rendez-vous", + ), + ), + ( + "job_seeker", + models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.CASCADE, + related_name="rdvi_participations", + to=settings.AUTH_USER_MODEL, + verbose_name="demandeur d'emploi", + ), + ), + ], + options={ + "verbose_name": "participation à un événement RDV-I", + "verbose_name_plural": "participations aux événements RDV-I", + }, + ), + migrations.AddField( + model_name="appointment", + name="participants", + field=models.ManyToManyField( + editable=False, + related_name="rdvi_appointments", + through="rdv_insertion.Participation", + to=settings.AUTH_USER_MODEL, + verbose_name="participants", + ), + ), migrations.AddConstraint( model_name="invitation", constraint=models.UniqueConstraint( diff --git a/itou/rdv_insertion/models.py b/itou/rdv_insertion/models.py index bc2eeca3c7f..2cd03f32bd8 100644 --- a/itou/rdv_insertion/models.py +++ b/itou/rdv_insertion/models.py @@ -2,11 +2,14 @@ from django.conf import settings from django.db import models +from django.utils import timezone -from .enums import InvitationStatus, InvitationType +from .enums import InvitationRequestReasonCategory, InvitationStatus, InvitationType, ParticipationStatus class InvitationRequest(models.Model): + ReasonCategory = InvitationRequestReasonCategory + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) job_seeker = models.ForeignKey( @@ -21,6 +24,7 @@ class InvitationRequest(models.Model): on_delete=models.CASCADE, related_name="rdvi_invitation_requests", ) + reason_category = models.CharField("catégorie de motif", choices=ReasonCategory.choices) created_at = models.DateTimeField("créée le", auto_now_add=True) api_response = models.JSONField(editable=False) @@ -31,6 +35,14 @@ class Meta: verbose_name = "demande d'invitation RDV-I" verbose_name_plural = "demandes d'invitation RDV-I" + @property + def email_invitation(self): + return next((invit for invit in self.invitations.all() if invit.is_email), None) + + @property + def sms_invitation(self): + return next((invit for invit in self.invitations.all() if invit.is_sms), None) + class Invitation(models.Model): Type = InvitationType @@ -38,14 +50,15 @@ class Invitation(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) type = models.CharField("format", choices=Type.choices) - status = models.CharField("état", default=Status.SENT, choices=InvitationStatus.choices) + status = models.CharField("état", default=Status.SENT, choices=Status.choices) + delivered_at = models.DateTimeField("délivrée le", null=True, editable=False) invitation_request = models.ForeignKey( "InvitationRequest", verbose_name="demande d'invitation", on_delete=models.CASCADE, related_name="invitations", ) - rdv_insertion_invitation_id = models.IntegerField(unique=True, editable=False) + rdv_insertion_id = models.IntegerField(unique=True, editable=False) class Meta: verbose_name = "invitation RDV-I" @@ -57,3 +70,127 @@ class Meta: violation_error_message="Une invitation de ce type existe déjà pour cette demande", ) ] + + @property + def is_email(self): + return self.type == self.Type.EMAIL + + @property + def is_postal(self): + return self.type == self.Type.POSTAL + + @property + def is_sms(self): + return self.type == self.Type.SMS + + +class Appointment(models.Model): + Status = ParticipationStatus + ReasonCategory = InvitationRequestReasonCategory + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + company = models.ForeignKey( + "companies.Company", + verbose_name="entreprise", + on_delete=models.CASCADE, + related_name="rdvi_appointments", + editable=False, + ) + participants = models.ManyToManyField( + settings.AUTH_USER_MODEL, + verbose_name="participants", + related_name="rdvi_appointments", + through="Participation", + editable=False, + ) + location = models.ForeignKey( + "Location", + null=True, + verbose_name="lieu", + on_delete=models.SET_NULL, + related_name="rdvi_appointments", + editable=False, + ) + + status = models.CharField("état", default=Status.UNKNOWN, choices=Status.choices, editable=False) + reason_category = models.CharField("catégorie de motif", choices=ReasonCategory.choices, editable=False) + reason = models.CharField("motif", editable=False) + is_collective = models.BooleanField("rendez-vous collectif", editable=False) + starts_at = models.DateTimeField("commence le", editable=False) + duration = models.DurationField("durée", editable=False) + canceled_at = models.DateTimeField("annulé le", null=True, editable=False) + address = models.CharField("adresse", editable=False) + total_participants = models.PositiveSmallIntegerField("nombre de participants", null=True, editable=False) + max_participants = models.PositiveSmallIntegerField("nombre max. de participants", null=True, editable=False) + rdv_insertion_id = models.IntegerField(unique=True, editable=False) + + class Meta: + verbose_name = "rendez-vous RDV-I" + verbose_name_plural = "rendez-vous RDV-I" + + +class Participation(models.Model): + Status = ParticipationStatus + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + job_seeker = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name="demandeur d'emploi", + on_delete=models.CASCADE, + related_name="rdvi_participations", + editable=False, + ) + appointment = models.ForeignKey( + "Appointment", + verbose_name="rendez-vous", + on_delete=models.CASCADE, + related_name="rdvi_participations", + editable=False, + ) + status = models.CharField("état", default=Status.UNKNOWN, choices=Status.choices, editable=False) + rdv_insertion_id = models.IntegerField(unique=True, editable=False) + + class Meta: + verbose_name = "participation à un événement RDV-I" + verbose_name_plural = "participations aux événements RDV-I" + + def get_status_display(self): + if self.status == self.Status.UNKNOWN: + if self.appointment.starts_at > timezone.now(): + return "RDV à venir" + return "Statut du RDV à préciser" + return self._get_FIELD_display(field=self._meta.get_field("status")) + + def get_status_class_name(self): + return { + self.Status.UNKNOWN: "bg-important-lightest text-important", + self.Status.SEEN: "bg-success-lighter text-success", + self.Status.REVOKED: "bg-warning-lighter text-warning", + self.Status.EXCUSED: "bg-warning-lighter text-warning", + self.Status.NOSHOW: "bg-danger-lighter text-danger", + }.get(self.status) + + +class Location(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + name = models.CharField("nom", editable=False) + address = models.CharField("adresse", editable=False) + phone_number = models.CharField("téléphone", null=True, editable=False) + rdv_insertion_id = models.IntegerField(unique=True, editable=False) + + class Meta: + verbose_name = "lieu d'un événement RDV-I" + verbose_name_plural = "lieux d'événements RDV-I" + + +class WebhookEvent(models.Model): + created_at = models.DateTimeField("créée le", auto_now_add=True) + body = models.JSONField(editable=False) + headers = models.JSONField(editable=False) + + class Meta: + verbose_name = "événement du webhook RDV-I" + verbose_name_plural = "événements du webhook RDV-I" diff --git a/itou/templates/apply/includes/appointments.html b/itou/templates/apply/includes/appointments.html new file mode 100644 index 00000000000..93e4c7fc5e3 --- /dev/null +++ b/itou/templates/apply/includes/appointments.html @@ -0,0 +1,91 @@ +{% load datetime_filters %} + +{% include "apply/includes/invitation_requests.html" with job_application=job_application invitation_requests=invitation_requests %} +{% if participations %} +
+

Rendez-vous

+ + + + + + + + + + + {% for participation in participations %} + + + + + + + {% endfor %} + +
StatutDate et heureMotif
+ {{ participation.get_status_display }} + {{ participation.appointment.starts_at }}{{ participation.appointment.reason }} + + +
+
+{% endif %} diff --git a/itou/templates/apply/includes/buttons/rdv_insertion_invite.html b/itou/templates/apply/includes/buttons/rdv_insertion_invite.html index 2f1481d3b54..cb571903499 100644 --- a/itou/templates/apply/includes/buttons/rdv_insertion_invite.html +++ b/itou/templates/apply/includes/buttons/rdv_insertion_invite.html @@ -1,13 +1,15 @@ {% load matomo %} -{% with state=state|default:"ok" %} +{% with state=state|default:"ok" for_detail=for_detail|default:False %} {% if state == "error" %} {% if job_application %} -
+ {% csrf_token %}
{% else %} - {% endif %} {% elif job_application.has_pending_rdv_insertion_invitation_request %} - {% else %} -
+ {% csrf_token %} + + + ''' +# --- +# name: TestRdvInsertionInvitationRequestsList.test_invitations_requests_listing + ''' +
+
+

Invitations envoyées

+ + + + + + + + + + + +
+ +

Aucune invitation en attente de réponse actuellement

+ +
+ ''' +# --- +# name: TestRdvInsertionInvitationRequestsList.test_invite_response_includes_updated_invitation_requests_listing[existing_invitation_requests] + ''' +
+
+

Invitations envoyées

+ + + + +
+ + +
+ + + +
+ +

Aucune invitation en attente de réponse actuellement

+ +
+ ''' +# --- +# name: TestRdvInsertionInvitationRequestsList.test_invite_response_includes_updated_invitation_requests_listing[updated_invitation_requests] + ''' +
+
+

Invitations envoyées

+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
Date d'envoiMotifInvitation SMSInvitation mail
1 août 2024 02:00Entretien SIAE-Délivrée
+ +
+ ''' +# --- diff --git a/tests/www/apply/__snapshots__/test_list.ambr b/tests/www/apply/__snapshots__/test_list.ambr index af482887f06..fd8389e2fb4 100644 --- a/tests/www/apply/__snapshots__/test_list.ambr +++ b/tests/www/apply/__snapshots__/test_list.ambr @@ -214,6 +214,7 @@ +
@@ -256,6 +257,7 @@ +
@@ -294,6 +296,7 @@ +
@@ -336,6 +339,7 @@ +
@@ -379,6 +383,7 @@ +
@@ -421,6 +426,7 @@ +
@@ -477,6 +483,7 @@ +
@@ -518,6 +525,7 @@ +
@@ -557,6 +565,7 @@ +
@@ -598,6 +607,7 @@ +
@@ -642,6 +652,7 @@ +
@@ -683,6 +694,7 @@ +
diff --git a/tests/www/apply/__snapshots__/test_list_rdv_insertion.ambr b/tests/www/apply/__snapshots__/test_list_rdv_insertion.ambr index c66fd0d38ee..4b2c8690373 100644 --- a/tests/www/apply/__snapshots__/test_list_rdv_insertion.ambr +++ b/tests/www/apply/__snapshots__/test_list_rdv_insertion.ambr @@ -1,57 +1,57 @@ # serializer version: 1 -# name: TestRDVInsertionView.test_rdv_insertion_configured_and_valid_rdv_insertion_exchange_with_no_pending_request +# name: TestRdvInsertionView.test_rdv_insertion_configured_and_valid_rdv_insertion_exchange_with_no_pending_request ''' - ''' # --- -# name: TestRDVInsertionView.test_rdv_insertion_configured_and_valid_rdv_insertion_exchange_with_pending_request[invitation_request_created] +# name: TestRdvInsertionView.test_rdv_insertion_configured_and_valid_rdv_insertion_exchange_with_pending_request[invitation_request_created] ''' - ''' # --- -# name: TestRDVInsertionView.test_rdv_insertion_configured_and_valid_rdv_insertion_exchange_with_pending_request[pending_invitation_request_still_too_recent] +# name: TestRdvInsertionView.test_rdv_insertion_configured_and_valid_rdv_insertion_exchange_with_pending_request[pending_invitation_request_still_too_recent] ''' - ''' # --- -# name: TestRDVInsertionView.test_rdv_insertion_configured_and_valid_rdv_insertion_exchange_with_pending_request[pending_invitation_request_too_recent] +# name: TestRdvInsertionView.test_rdv_insertion_configured_and_valid_rdv_insertion_exchange_with_pending_request[pending_invitation_request_too_recent] ''' - ''' # --- -# name: TestRDVInsertionView.test_rdv_insertion_configured_invalid_job_application +# name: TestRdvInsertionView.test_rdv_insertion_configured_invalid_job_application ''' - ''' # --- -# name: TestRDVInsertionView.test_rdv_insertion_configured_with_failed_rdv_insertion_exchange +# name: TestRdvInsertionView.test_rdv_insertion_configured_with_failed_rdv_insertion_exchange '''
-
diff --git a/tests/www/apply/__snapshots__/test_process.ambr b/tests/www/apply/__snapshots__/test_process.ambr index 6606966c4bc..f717895ad02 100644 --- a/tests/www/apply/__snapshots__/test_process.ambr +++ b/tests/www/apply/__snapshots__/test_process.ambr @@ -207,6 +207,17 @@ "job_applications_jobapplication"."planned_training_hours", "job_applications_jobapplication"."inverted_vae_contract", "job_applications_jobapplication"."diagoriente_invite_sent_at", + EXISTS + (SELECT %s AS "a" + FROM "rdv_insertion_invitationrequest" U0 + WHERE (U0."company_id" = ("job_applications_jobapplication"."to_company_id") + AND U0."created_at" > %s + AND U0."job_seeker_id" = ("job_applications_jobapplication"."job_seeker_id")) + LIMIT 1) AS "has_pending_rdv_insertion_invitation_request", + COUNT("rdv_insertion_participation"."id") FILTER ( + WHERE ("rdv_insertion_appointment"."company_id" = %s + AND "rdv_insertion_appointment"."starts_at" > %s + AND "rdv_insertion_participation"."status" = %s)) AS "upcoming_participations_count", T5."id", T5."password", T5."last_login", @@ -278,38 +289,38 @@ "eligibility_eligibilitydiagnosis"."expires_at", "eligibility_eligibilitydiagnosis"."job_seeker_id", "eligibility_eligibilitydiagnosis"."author_siae_id", - T8."id", - T8."password", - T8."last_login", - T8."is_superuser", - T8."username", - T8."first_name", - T8."last_name", - T8."is_staff", - T8."is_active", - T8."date_joined", - T8."address_line_1", - T8."address_line_2", - T8."post_code", - T8."city", - T8."department", - T8."coords", - T8."geocoding_score", - T8."geocoding_updated_at", - T8."ban_api_resolved_address", - T8."insee_city_id", - T8."title", - T8."email", - T8."phone", - T8."kind", - T8."identity_provider", - T8."has_completed_welcoming_tour", - T8."created_by_id", - T8."external_data_source_history", - T8."last_checked_at", - T8."public_id", - T8."address_filled_at", - T8."first_login", + T11."id", + T11."password", + T11."last_login", + T11."is_superuser", + T11."username", + T11."first_name", + T11."last_name", + T11."is_staff", + T11."is_active", + T11."date_joined", + T11."address_line_1", + T11."address_line_2", + T11."post_code", + T11."city", + T11."department", + T11."coords", + T11."geocoding_score", + T11."geocoding_updated_at", + T11."ban_api_resolved_address", + T11."insee_city_id", + T11."title", + T11."email", + T11."phone", + T11."kind", + T11."identity_provider", + T11."has_completed_welcoming_tour", + T11."created_by_id", + T11."external_data_source_history", + T11."last_checked_at", + T11."public_id", + T11."address_filled_at", + T11."first_login", "prescribers_prescriberorganization"."id", "prescribers_prescriberorganization"."address_line_1", "prescribers_prescriberorganization"."address_line_2", @@ -340,142 +351,68 @@ "prescribers_prescriberorganization"."authorization_status", "prescribers_prescriberorganization"."authorization_updated_at", "prescribers_prescriberorganization"."authorization_updated_by_id", - T10."id", - T10."password", - T10."last_login", - T10."is_superuser", - T10."username", - T10."first_name", - T10."last_name", - T10."is_staff", - T10."is_active", - T10."date_joined", - T10."address_line_1", - T10."address_line_2", - T10."post_code", - T10."city", - T10."department", - T10."coords", - T10."geocoding_score", - T10."geocoding_updated_at", - T10."ban_api_resolved_address", - T10."insee_city_id", - T10."title", - T10."email", - T10."phone", - T10."kind", - T10."identity_provider", - T10."has_completed_welcoming_tour", - T10."created_by_id", - T10."external_data_source_history", - T10."last_checked_at", - T10."public_id", - T10."address_filled_at", - T10."first_login", - T11."user_id", - T11."birthdate", - T11."birth_place_id", - T11."birth_country_id", - T11."nir", - T11."lack_of_nir_reason", - T11."pole_emploi_id", - T11."lack_of_pole_emploi_id_reason", - T11."asp_uid", - T11."education_level", - T11."resourceless", - T11."rqth_employee", - T11."oeth_employee", - T11."pole_emploi_since", - T11."unemployed_since", - T11."has_rsa_allocation", - T11."rsa_allocation_since", - T11."ass_allocation_since", - T11."aah_allocation_since", - T11."ata_allocation_since", - T11."hexa_lane_number", - T11."hexa_std_extension", - T11."hexa_non_std_extension", - T11."hexa_lane_type", - T11."hexa_lane_name", - T11."hexa_additional_address", - T11."hexa_post_code", - T11."hexa_commune_id", - T11."pe_obfuscated_nir", - T11."pe_last_certification_attempt_at", - T12."id", - T12."address_line_1", - T12."address_line_2", - T12."post_code", - T12."city", - T12."department", - T12."coords", - T12."geocoding_score", - T12."geocoding_updated_at", - T12."ban_api_resolved_address", - T12."insee_city_id", - T12."name", - T12."created_at", - T12."updated_at", - T12."uid", - T12."active_members_email_reminder_last_sent_at", - T12."siret", - T12."naf", - T12."kind", - T12."brand", - T12."phone", - T12."email", - T12."auth_email", - T12."website", - T12."description", - T12."provided_support", - T12."source", - T12."created_by_id", - T12."block_job_applications", - T12."job_applications_blocked_at", - T12."convention_id", - T12."job_app_score", - T12."rdv_solidarites_id", - "eligibility_geiqeligibilitydiagnosis"."id", - "eligibility_geiqeligibilitydiagnosis"."author_id", - "eligibility_geiqeligibilitydiagnosis"."author_kind", - "eligibility_geiqeligibilitydiagnosis"."author_prescriber_organization_id", - "eligibility_geiqeligibilitydiagnosis"."created_at", - "eligibility_geiqeligibilitydiagnosis"."updated_at", - "eligibility_geiqeligibilitydiagnosis"."expires_at", - "eligibility_geiqeligibilitydiagnosis"."job_seeker_id", - "eligibility_geiqeligibilitydiagnosis"."author_geiq_id", - T14."id", - T14."password", - T14."last_login", - T14."is_superuser", - T14."username", - T14."first_name", - T14."last_name", - T14."is_staff", - T14."is_active", - T14."date_joined", - T14."address_line_1", - T14."address_line_2", - T14."post_code", - T14."city", - T14."department", - T14."coords", - T14."geocoding_score", - T14."geocoding_updated_at", - T14."ban_api_resolved_address", - T14."insee_city_id", - T14."title", - T14."email", - T14."phone", - T14."kind", - T14."identity_provider", - T14."has_completed_welcoming_tour", - T14."created_by_id", - T14."external_data_source_history", - T14."last_checked_at", - T14."public_id", - T14."address_filled_at", - T14."first_login", + T13."id", + T13."password", + T13."last_login", + T13."is_superuser", + T13."username", + T13."first_name", + T13."last_name", + T13."is_staff", + T13."is_active", + T13."date_joined", + T13."address_line_1", + T13."address_line_2", + T13."post_code", + T13."city", + T13."department", + T13."coords", + T13."geocoding_score", + T13."geocoding_updated_at", + T13."ban_api_resolved_address", + T13."insee_city_id", + T13."title", + T13."email", + T13."phone", + T13."kind", + T13."identity_provider", + T13."has_completed_welcoming_tour", + T13."created_by_id", + T13."external_data_source_history", + T13."last_checked_at", + T13."public_id", + T13."address_filled_at", + T13."first_login", + T14."user_id", + T14."birthdate", + T14."birth_place_id", + T14."birth_country_id", + T14."nir", + T14."lack_of_nir_reason", + T14."pole_emploi_id", + T14."lack_of_pole_emploi_id_reason", + T14."asp_uid", + T14."education_level", + T14."resourceless", + T14."rqth_employee", + T14."oeth_employee", + T14."pole_emploi_since", + T14."unemployed_since", + T14."has_rsa_allocation", + T14."rsa_allocation_since", + T14."ass_allocation_since", + T14."aah_allocation_since", + T14."ata_allocation_since", + T14."hexa_lane_number", + T14."hexa_std_extension", + T14."hexa_non_std_extension", + T14."hexa_lane_type", + T14."hexa_lane_name", + T14."hexa_additional_address", + T14."hexa_post_code", + T14."hexa_commune_id", + T14."pe_obfuscated_nir", + T14."pe_last_certification_attempt_at", T15."id", T15."address_line_1", T15."address_line_2", @@ -509,36 +446,110 @@ T15."convention_id", T15."job_app_score", T15."rdv_solidarites_id", - T16."id", - T16."address_line_1", - T16."address_line_2", - T16."post_code", - T16."city", - T16."department", - T16."coords", - T16."geocoding_score", - T16."geocoding_updated_at", - T16."ban_api_resolved_address", - T16."insee_city_id", - T16."name", - T16."created_at", - T16."updated_at", - T16."uid", - T16."active_members_email_reminder_last_sent_at", - T16."siret", - T16."is_head_office", - T16."kind", - T16."is_brsa", - T16."phone", - T16."email", - T16."website", - T16."description", - T16."is_authorized", - T16."code_safir_pole_emploi", - T16."created_by_id", - T16."authorization_status", - T16."authorization_updated_at", - T16."authorization_updated_by_id", + "eligibility_geiqeligibilitydiagnosis"."id", + "eligibility_geiqeligibilitydiagnosis"."author_id", + "eligibility_geiqeligibilitydiagnosis"."author_kind", + "eligibility_geiqeligibilitydiagnosis"."author_prescriber_organization_id", + "eligibility_geiqeligibilitydiagnosis"."created_at", + "eligibility_geiqeligibilitydiagnosis"."updated_at", + "eligibility_geiqeligibilitydiagnosis"."expires_at", + "eligibility_geiqeligibilitydiagnosis"."job_seeker_id", + "eligibility_geiqeligibilitydiagnosis"."author_geiq_id", + T17."id", + T17."password", + T17."last_login", + T17."is_superuser", + T17."username", + T17."first_name", + T17."last_name", + T17."is_staff", + T17."is_active", + T17."date_joined", + T17."address_line_1", + T17."address_line_2", + T17."post_code", + T17."city", + T17."department", + T17."coords", + T17."geocoding_score", + T17."geocoding_updated_at", + T17."ban_api_resolved_address", + T17."insee_city_id", + T17."title", + T17."email", + T17."phone", + T17."kind", + T17."identity_provider", + T17."has_completed_welcoming_tour", + T17."created_by_id", + T17."external_data_source_history", + T17."last_checked_at", + T17."public_id", + T17."address_filled_at", + T17."first_login", + T18."id", + T18."address_line_1", + T18."address_line_2", + T18."post_code", + T18."city", + T18."department", + T18."coords", + T18."geocoding_score", + T18."geocoding_updated_at", + T18."ban_api_resolved_address", + T18."insee_city_id", + T18."name", + T18."created_at", + T18."updated_at", + T18."uid", + T18."active_members_email_reminder_last_sent_at", + T18."siret", + T18."naf", + T18."kind", + T18."brand", + T18."phone", + T18."email", + T18."auth_email", + T18."website", + T18."description", + T18."provided_support", + T18."source", + T18."created_by_id", + T18."block_job_applications", + T18."job_applications_blocked_at", + T18."convention_id", + T18."job_app_score", + T18."rdv_solidarites_id", + T19."id", + T19."address_line_1", + T19."address_line_2", + T19."post_code", + T19."city", + T19."department", + T19."coords", + T19."geocoding_score", + T19."geocoding_updated_at", + T19."ban_api_resolved_address", + T19."insee_city_id", + T19."name", + T19."created_at", + T19."updated_at", + T19."uid", + T19."active_members_email_reminder_last_sent_at", + T19."siret", + T19."is_head_office", + T19."kind", + T19."is_brsa", + T19."phone", + T19."email", + T19."website", + T19."description", + T19."is_authorized", + T19."code_safir_pole_emploi", + T19."created_by_id", + T19."authorization_status", + T19."authorization_updated_at", + T19."authorization_updated_by_id", "companies_company"."id", "companies_company"."address_line_1", "companies_company"."address_line_2", @@ -572,38 +583,38 @@ "companies_company"."convention_id", "companies_company"."job_app_score", "companies_company"."rdv_solidarites_id", - T17."id", - T17."password", - T17."last_login", - T17."is_superuser", - T17."username", - T17."first_name", - T17."last_name", - T17."is_staff", - T17."is_active", - T17."date_joined", - T17."address_line_1", - T17."address_line_2", - T17."post_code", - T17."city", - T17."department", - T17."coords", - T17."geocoding_score", - T17."geocoding_updated_at", - T17."ban_api_resolved_address", - T17."insee_city_id", - T17."title", - T17."email", - T17."phone", - T17."kind", - T17."identity_provider", - T17."has_completed_welcoming_tour", - T17."created_by_id", - T17."external_data_source_history", - T17."last_checked_at", - T17."public_id", - T17."address_filled_at", - T17."first_login", + T20."id", + T20."password", + T20."last_login", + T20."is_superuser", + T20."username", + T20."first_name", + T20."last_name", + T20."is_staff", + T20."is_active", + T20."date_joined", + T20."address_line_1", + T20."address_line_2", + T20."post_code", + T20."city", + T20."department", + T20."coords", + T20."geocoding_score", + T20."geocoding_updated_at", + T20."ban_api_resolved_address", + T20."insee_city_id", + T20."title", + T20."email", + T20."phone", + T20."kind", + T20."identity_provider", + T20."has_completed_welcoming_tour", + T20."created_by_id", + T20."external_data_source_history", + T20."last_checked_at", + T20."public_id", + T20."address_filled_at", + T20."first_login", "approvals_approval"."id", "approvals_approval"."start_at", "approvals_approval"."end_at", @@ -627,22 +638,40 @@ INNER JOIN "companies_companymembership" ON ("companies_company"."id" = "companies_companymembership"."company_id") INNER JOIN "users_user" ON ("companies_companymembership"."user_id" = "users_user"."id") INNER JOIN "users_user" T5 ON ("job_applications_jobapplication"."job_seeker_id" = T5."id") + LEFT OUTER JOIN "rdv_insertion_participation" ON (T5."id" = "rdv_insertion_participation"."job_seeker_id") + LEFT OUTER JOIN "rdv_insertion_appointment" ON ("rdv_insertion_participation"."appointment_id" = "rdv_insertion_appointment"."id") LEFT OUTER JOIN "users_jobseekerprofile" ON (T5."id" = "users_jobseekerprofile"."user_id") LEFT OUTER JOIN "eligibility_eligibilitydiagnosis" ON ("job_applications_jobapplication"."eligibility_diagnosis_id" = "eligibility_eligibilitydiagnosis"."id") - LEFT OUTER JOIN "users_user" T8 ON ("eligibility_eligibilitydiagnosis"."author_id" = T8."id") + LEFT OUTER JOIN "users_user" T11 ON ("eligibility_eligibilitydiagnosis"."author_id" = T11."id") LEFT OUTER JOIN "prescribers_prescriberorganization" ON ("eligibility_eligibilitydiagnosis"."author_prescriber_organization_id" = "prescribers_prescriberorganization"."id") - LEFT OUTER JOIN "users_user" T10 ON ("eligibility_eligibilitydiagnosis"."job_seeker_id" = T10."id") - LEFT OUTER JOIN "users_jobseekerprofile" T11 ON (T10."id" = T11."user_id") - LEFT OUTER JOIN "companies_company" T12 ON ("eligibility_eligibilitydiagnosis"."author_siae_id" = T12."id") + LEFT OUTER JOIN "users_user" T13 ON ("eligibility_eligibilitydiagnosis"."job_seeker_id" = T13."id") + LEFT OUTER JOIN "users_jobseekerprofile" T14 ON (T13."id" = T14."user_id") + LEFT OUTER JOIN "companies_company" T15 ON ("eligibility_eligibilitydiagnosis"."author_siae_id" = T15."id") LEFT OUTER JOIN "eligibility_geiqeligibilitydiagnosis" ON ("job_applications_jobapplication"."geiq_eligibility_diagnosis_id" = "eligibility_geiqeligibilitydiagnosis"."id") - LEFT OUTER JOIN "users_user" T14 ON ("job_applications_jobapplication"."sender_id" = T14."id") - LEFT OUTER JOIN "companies_company" T15 ON ("job_applications_jobapplication"."sender_company_id" = T15."id") - LEFT OUTER JOIN "prescribers_prescriberorganization" T16 ON ("job_applications_jobapplication"."sender_prescriber_organization_id" = T16."id") - LEFT OUTER JOIN "users_user" T17 ON ("job_applications_jobapplication"."archived_by_id" = T17."id") + LEFT OUTER JOIN "users_user" T17 ON ("job_applications_jobapplication"."sender_id" = T17."id") + LEFT OUTER JOIN "companies_company" T18 ON ("job_applications_jobapplication"."sender_company_id" = T18."id") + LEFT OUTER JOIN "prescribers_prescriberorganization" T19 ON ("job_applications_jobapplication"."sender_prescriber_organization_id" = T19."id") + LEFT OUTER JOIN "users_user" T20 ON ("job_applications_jobapplication"."archived_by_id" = T20."id") LEFT OUTER JOIN "approvals_approval" ON ("job_applications_jobapplication"."approval_id" = "approvals_approval"."id") WHERE ("companies_companymembership"."user_id" = %s AND "users_user"."is_active" AND "job_applications_jobapplication"."id" = %s) + GROUP BY "job_applications_jobapplication"."id", + T5."id", + "users_jobseekerprofile"."user_id", + "eligibility_eligibilitydiagnosis"."id", + T11."id", + "prescribers_prescriberorganization"."id", + T13."id", + T14."user_id", + T15."id", + "eligibility_geiqeligibilitydiagnosis"."id", + T17."id", + T18."id", + T19."id", + "companies_company"."id", + T20."id", + "approvals_approval"."id" LIMIT 21 ''', }), diff --git a/tests/www/apply/test_detail_rdv_insertion.py b/tests/www/apply/test_detail_rdv_insertion.py new file mode 100644 index 00000000000..b46e8c3c4da --- /dev/null +++ b/tests/www/apply/test_detail_rdv_insertion.py @@ -0,0 +1,324 @@ +import datetime +from urllib.parse import urljoin + +import httpx +import pytest +import respx +from django.urls import reverse +from freezegun import freeze_time +from pytest_django.asserts import assertContains, assertNotContains, assertTemplateUsed + +from itou.rdv_insertion.models import Appointment, InvitationRequest, Participation +from itou.utils.mocks.rdv_insertion import ( + RDV_INSERTION_AUTH_SUCCESS_HEADERS, + RDV_INSERTION_CREATE_AND_INVITE_SUCCESS_BODY, +) +from tests.job_applications.factories import JobApplicationFactory +from tests.prescribers.factories import PrescriberOrganizationWithMembershipFactory +from tests.rdv_insertion.factories import InvitationRequestFactory, ParticipationFactory +from tests.utils.test import parse_response_to_soup + + +@pytest.fixture(autouse=True) +def mock_rdvs_api(settings): + settings.RDV_SOLIDARITES_API_BASE_URL = "https://rdv-solidarites.fake/api/v1/" + settings.RDV_SOLIDARITES_EMAIL = "tech@inclusion.beta.gouv.fr" + settings.RDV_SOLIDARITES_PASSWORD = "password" + settings.RDV_INSERTION_API_BASE_URL = "https://rdv-insertion.fake/api/v1/" + settings.RDV_INSERTION_INVITE_HOLD_DURATION = datetime.timedelta(days=2) + + respx.post( + urljoin( + settings.RDV_INSERTION_API_BASE_URL, + "organisations/1234/users/create_and_invite", + ), + name="rdv_solidarites_create_and_invite", + ).mock( + return_value=httpx.Response( + 200, + json=RDV_INSERTION_CREATE_AND_INVITE_SUCCESS_BODY, + ) + ) + + +@freeze_time("2024-08-01") +class TestRdvInsertionAppointmentsList: + APPOINTMENTS_TAB_TITLE = "Rendez-vous" + + def setup_method(self, freeze, **kwargs): + organization = PrescriberOrganizationWithMembershipFactory( + membership__user__first_name="Max", membership__user__last_name="Throughput" + ) + self.job_application = JobApplicationFactory( + to_company__name="Hit Pit", + to_company__with_membership=True, + to_company__rdv_solidarites_id=1234, + job_seeker__first_name="Jacques", + job_seeker__last_name="Henry", + sender=organization.active_members.get(), + for_snapshot=True, + ) + self.participation = ParticipationFactory( + job_seeker=self.job_application.job_seeker, + appointment__company=self.job_application.to_company, + appointment__starts_at=datetime.datetime(2024, 9, 1, 8, 0, tzinfo=datetime.UTC), + for_snapshot=True, + ) + + def test_details_should_not_include_appointments_tab_is_not_configured(self, client): + self.job_application.to_company.rdv_solidarites_id = None + self.job_application.to_company.save() + self.participation.appointment.delete() + + client.force_login(self.job_application.to_company.members.get()) + response = client.get( + reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.pk}) + ) + assertTemplateUsed(response, "apply/process_details_company.html") + assertNotContains(response, self.APPOINTMENTS_TAB_TITLE) + + def test_details_should_include_appointments_tab_is_not_configured_and_has_upcoming_appointments(self, client): + self.job_application.to_company.rdv_solidarites_id = None + self.job_application.to_company.save() + + client.force_login(self.job_application.to_company.members.get()) + response = client.get( + reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.pk}) + ) + assertTemplateUsed(response, "apply/process_details_company.html") + assertContains(response, self.APPOINTMENTS_TAB_TITLE) + + def test_details_should_include_appointments_tab_is_configured_and_without_upcoming_appointments(self, client): + self.participation.appointment.delete() + + client.force_login(self.job_application.to_company.members.get()) + response = client.get( + reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.pk}) + ) + assertTemplateUsed(response, "apply/process_details_company.html") + assertContains(response, self.APPOINTMENTS_TAB_TITLE) + + def test_details_should_include_appointments_tab_is_configured_and_with_upcoming_appointments(self, client): + client.force_login(self.job_application.to_company.members.get()) + response = client.get( + reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.pk}) + ) + assertTemplateUsed(response, "apply/process_details_company.html") + assertContains(response, self.APPOINTMENTS_TAB_TITLE) + + def test_appointments_tab_should_not_display_appointments_table_when_no_appointments_exist(self, client): + self.participation.appointment.delete() + + client.force_login(self.job_application.to_company.members.get()) + response = client.get( + reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.pk}) + ) + assertNotContains(response, '1' + ) + + # Past participation + ParticipationFactory( + job_seeker=self.job_application.job_seeker, + appointment__company=self.job_application.to_company, + appointment__status=Appointment.Status.UNKNOWN, + appointment__starts_at=datetime.datetime(2024, 6, 3, 8, 0, tzinfo=datetime.UTC), + status=Participation.Status.UNKNOWN, + ) + # Future participation + ParticipationFactory( + job_seeker=self.job_application.job_seeker, + appointment__company=self.job_application.to_company, + appointment__status=Appointment.Status.UNKNOWN, + appointment__starts_at=datetime.datetime(2024, 9, 2, 8, 0, tzinfo=datetime.UTC), + status=Participation.Status.UNKNOWN, + ) + + response = client.get( + reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.pk}) + ) + assertContains( + response, '2' + ) + + # Delete appointments + self.job_application.job_seeker.rdvi_appointments.all().delete() + response = client.get( + reverse("apply:details_for_company", kwargs={"job_application_id": self.job_application.pk}) + ) + assertNotContains(response, '