diff --git a/.talismanrc b/.talismanrc index 9fd8faed40..dd804e7037 100644 --- a/.talismanrc +++ b/.talismanrc @@ -16,7 +16,7 @@ fileignoreconfig: - filename: erp/export/static/schema.json checksum: 888e09b1fe8ead28a4561b05842cd852be1ca8a3b86264aa48469514f93cfdd2 - filename: templates/contrib/6-accueil.html - checksum: 6a2b50478a202eb35bf49d50ff156110eeed68287d545626753b7343bd3e3074 + checksum: 8aaacce11ed6b610a2bd7556d5f42b5035747d994eb103ccdfe0fdec0ae1c708 - filename: templates/contrib/2-a-propos.html checksum: 674d89a3766a1df233648b7de14a5116de651bfea5b4db7f4f9f05f95629a32a - filename: templates/contrib/0-start.html @@ -28,7 +28,7 @@ fileignoreconfig: - filename: templates/contrib/4-exterieur.html checksum: 4e6911a34b51f640e0fb6a2dad359628daf63459f2b3d868ce0d80ce094f8538 - filename: templates/contrib/3-transport.html - checksum: 31cb1838db8607f44c92e00e0841c18f7454943ea3a600d79f943fae2f967874 + checksum: 68334d56087d71ffb7443ca8917ed32c553786a8b1c9852bec1adec03aa138fe - filename: .circleci/config.yml checksum: 9c50da347ce5a9602eb25d2c192597343c50e062f465c0e8ae6e4aed1034407e - filename: erp/imports/mapper/service_public.py @@ -52,7 +52,13 @@ fileignoreconfig: - filename: erp/management/commands/flush_widget_stats.py checksum: 07f7fb0fbd7c587d43aad69f87e892107a6e80c8d5a5145d608905485a4f2011 - filename: tests/stats/tests.py - checksum: 3d899b94bc836e0f97b282d569ca27b513be6df54cf718e96a944a99b634444d + checksum: 14316ca4cb3801fde82de4647e7c3df271a7c5b962e3526fa06e5d190caceb27 +- filename: tests/api/tests.py + checksum: 19cc87e6a7c33d056a2c85831e86af11ba1fff4935b9fc4b786faee0c88552c9 +- filename: tests/erp/test_tasks.py + checksum: 5af076ad122ca09cd7a76c75a11c3094c70d15fa928a827a3180c8c43177a9f5 +- filename: tests/erp/imports/test_serializers.py + checksum: ec01ecc27f54e45f02eae36db0369b353e069426f19eeac2025d776760f94fec - filename: .env.sample checksum: aa3f02c8f5d30f989986f9eafaca8bc57d99d09361b9fa9c2158638bcc263f15 - filename: erp/management/commands/import_apidae.py @@ -93,3 +99,9 @@ fileignoreconfig: checksum: 40161683c6d36b3a1d5bb3c0b05594fa48ada9dd88ecc379294fa10f64743eb2 - filename: core/settings.py checksum: 541b4298f5183e65805309ef55140f6b1da02f62a1fbf196fba3cc8d7c5dab58 +- filename: erp/models.py + checksum: 7399cf142d418045c6167e0ed5b83c3f61a9102015d994929e0584f288508e67 +- filename: erp/schema.py + checksum: 319eee5047b7058a601da24eb201f2ed8fd4ece23e9dc47a92f05f2e956e7ae2 +- filename: erp/migrations/0191_remove_accessibilite_erp_accessibilite_sanitaires_presence_consistency_and_more.py + checksum: 9549fe2888e7d35c031c145eca2235510545a0b69774201c1365f466ca42f649 \ No newline at end of file diff --git a/erp/forms.py b/erp/forms.py index 2f0f8a1967..1a92224b51 100644 --- a/erp/forms.py +++ b/erp/forms.py @@ -316,6 +316,115 @@ def clean_accueil_soignant_experience(self): return self.cleaned_data["accueil_soignant_experience"] +class ContribAccessibiliteSportsEquipmentForm(ContribAccessibiliteForm): + accueil_casiers_fermeture = forms.MultipleChoiceField( + required=False, + choices=schema.ACCUEIL_SYSTEME_FERMETURE_CHOICES, + widget=forms.CheckboxSelectMultiple, + label=schema.get_label("accueil_casiers_fermeture"), + help_text=schema.get_help_text("accueil_casiers_fermeture"), + ) + + accueil_prestations_complementaires = forms.MultipleChoiceField( + required=False, + choices=schema.ACCUEIL_PRESTATIONS_COMPLEMENTAIRES_CHOICES, + widget=forms.CheckboxSelectMultiple, + label=schema.get_label("accueil_prestations_complementaires"), + help_text=schema.get_help_text("accueil_prestations_complementaires"), + ) + + accueil_presence_espaces_specifiques = forms.MultipleChoiceField( + required=False, + choices=schema.ACCUEIL_PRESENCE_ESPACES_SPECIFIQUES_CHOICES, + widget=forms.CheckboxSelectMultiple, + label=schema.get_label("accueil_presence_espaces_specifiques"), + help_text=schema.get_help_text("accueil_presence_espaces_specifiques"), + ) + + fields_to_remove = ( + "accueil_personnels", # Root + "accueil_audiodescription_presence", # Root + "accueil_audiodescription", # Child + "accueil_ascenseur_etage", # Root + "accueil_ascenseur_etage_pmr", # Child + "labels", # Root + "labels_familles_handicap", # Root + "labels_autre", # Root + ) + + large_establishments_fields_to_keep = ["accueil_signaletique_interieure", "cheminement_ext_signaletique_exterieure"] + conditionals_to_add = get_conditional_fields_in(["sports_equipment"]) + large_establishments_fields_to_keep + conditionals_to_remove = get_conditional_fields_not_in(["sports_equipment"]) + + class Meta: + model = Accessibilite + exclude = ("pk",) + widgets = get_widgets_for_accessibilite() + labels = schema.get_labels(include_conditional=["sports_equipment", "large_establishments"]) + help_texts = schema.get_help_texts(include_conditional=["sports_equipment", "large_establishments"]) + required = schema.get_required_fields() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + allowed_fields = set(schema.get_help_texts(include_conditional=["sports_equipment"]).keys()) | set( + self.large_establishments_fields_to_keep + ) + for field in copy(self.fields): + if field not in allowed_fields: + self.fields.pop(field, None) + + def clean_accueil_casiers_adaptes(self): + if self.cleaned_data.get("accueil_casiers") is not True: + return None + return self.cleaned_data["accueil_casiers_adaptes"] + + def clean_accueil_casiers_fermeture(self): + if self.cleaned_data.get("accueil_casiers") is not True: + return None + return self.cleaned_data["accueil_casiers_fermeture"] + + def clean_accueil_tribunes_localisation_places(self): + if (self.cleaned_data.get("accueil_tribunes_places") or 0) < 1: + return None + return self.cleaned_data["accueil_tribunes_localisation_places"] + + def clean_accueil_tribunes_places_avec_accompagnants(self): + if (self.cleaned_data.get("accueil_tribunes_places") or 0) < 1: + return None + return self.cleaned_data["accueil_tribunes_places_avec_accompagnants"] + + def clean_accueil_vestiaires_largeur_passage(self): + if self.cleaned_data.get("accueil_vestiaires") is not True: + return None + return self.cleaned_data["accueil_vestiaires_largeur_passage"] + + def clean_accueil_douches_collectives_adaptees(self): + if self.cleaned_data.get("accueil_douches_collectives") is not True: + return None + return self.cleaned_data["accueil_douches_collectives_adaptees"] + + def clean_accueil_douches_individuelles_adaptees(self): + if self.cleaned_data.get("accueil_douches_individuelles") is not True: + return None + return self.cleaned_data["accueil_douches_individuelles_adaptees"] + + def clean_sanitaires_urinoirs(self): + if self.cleaned_data.get("sanitaires_presence") is not True: + return None + return self.cleaned_data["sanitaires_urinoirs"] + + def clean_sanitaires_largeur_porte(self): + if self.cleaned_data.get("sanitaires_adaptes") is not True: + return None + return self.cleaned_data["sanitaires_largeur_porte"] + + def clean_sanitaires_sens_transfert(self): + if self.cleaned_data.get("sanitaires_adaptes") is not True: + return None + return self.cleaned_data["sanitaires_sens_transfert"] + + class AdminAccessibiliteForm(ContribAccessibiliteForm): # Note: defining `labels` and `help_texts` in `Meta` doesn't work with custom # fields, hence why we set them up manually for each fields. @@ -942,6 +1051,7 @@ def get_contrib_forms_for_activity(activity: Activite): ACTIVITY_GROUPS["LARGE_ESTABLISHMENTS"]: ContribAccessibiliteLargeEstablishmentsForm, ACTIVITY_GROUPS["POLLING_STATION"]: ContribAccessibilitePollingStationForm, ACTIVITY_GROUPS["HEALTHCARE"]: ContribAccessibiliteHealthcareForm, + ACTIVITY_GROUPS["SPORTS_EQUIPMENT"]: ContribAccessibiliteSportsEquipmentForm, } groups = activity.groups.all() diff --git a/erp/imports/serializers.py b/erp/imports/serializers.py index 5cc43fadea..0027e98a7f 100644 --- a/erp/imports/serializers.py +++ b/erp/imports/serializers.py @@ -53,6 +53,7 @@ class AccessibilityImportSerializer(serializers.ModelSerializer): stationnement_pmr = NullBooleanField(required=False, allow_null=True) stationnement_ext_presence = NullBooleanField(required=False, allow_null=True) stationnement_ext_pmr = NullBooleanField(required=False, allow_null=True) + stationnement_zone_depose_pmr = NullBooleanField(required=False, allow_null=True) cheminement_ext_presence = NullBooleanField(required=False, allow_null=True) cheminement_ext_terrain_stable = NullBooleanField(required=False, allow_null=True) cheminement_ext_plain_pied = NullBooleanField(required=False, allow_null=True) @@ -95,8 +96,18 @@ class AccessibilityImportSerializer(serializers.ModelSerializer): accueil_soignant = NullBooleanField(required=False, allow_null=True) accueil_salle_consultation_accessible = NullBooleanField(required=False, allow_null=True) accueil_consultation_domicile = NullBooleanField(required=False, allow_null=True) + accueil_aire_de_jeux = NullBooleanField(required=False, allow_null=True) + accueil_tribunes = NullBooleanField(required=False, allow_null=True) + accueil_vestiaires = NullBooleanField(required=False, allow_null=True) + accueil_douches_collectives = NullBooleanField(required=False, allow_null=True) + accueil_douches_collectives_adaptees = NullBooleanField(required=False, allow_null=True) + accueil_douches_individuelles = NullBooleanField(required=False, allow_null=True) + accueil_douches_individuelles_adaptees = NullBooleanField(required=False, allow_null=True) + accueil_casiers = NullBooleanField(required=False, allow_null=True) + accueil_casiers_adaptes = NullBooleanField(required=False, allow_null=True) sanitaires_presence = NullBooleanField(required=False, allow_null=True) sanitaires_adaptes = NullBooleanField(required=False, allow_null=True) + sanitaires_urinoirs = NullBooleanField(required=False, allow_null=True) conformite = NullBooleanField(required=False, allow_null=True) class Meta: @@ -222,15 +233,22 @@ def validate_accessibilite(self, obj): return obj + @staticmethod + def _should_display_children(field_config, current_value): + min_value = field_config.get("min_value_to_display_children") + if min_value is not None: + return current_value is not None and current_value >= min_value + + return str(current_value) in field_config.get("value_to_display_children", []) + def _handle_children_reinit(self, accessibilite_instance, field_name): field_config = FIELDS.get(field_name) if not field_config or not field_config.get("children"): return current_value = getattr(accessibilite_instance, field_name) - trigger_values = field_config.get("value_to_display_children", []) - if str(current_value) not in trigger_values: + if not self._should_display_children(field_config, current_value): for child_name in field_config.get("children"): field_obj = accessibilite_instance._meta.get_field(child_name) default_value = field_obj.get_default() diff --git a/erp/migrations/0191_remove_accessibilite_erp_accessibilite_sanitaires_presence_consistency_and_more.py b/erp/migrations/0191_remove_accessibilite_erp_accessibilite_sanitaires_presence_consistency_and_more.py new file mode 100644 index 0000000000..a77c0147c2 --- /dev/null +++ b/erp/migrations/0191_remove_accessibilite_erp_accessibilite_sanitaires_presence_consistency_and_more.py @@ -0,0 +1,428 @@ +# Generated by Django 6.0.7 on 2026-08-03 20:32 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("erp", "0190_rpa"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="accessibilite", + name="erp_accessibilite_sanitaires_presence_consistency", + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_aire_de_jeux", + field=models.BooleanField( + blank=True, + choices=[(True, "Oui"), (False, "Non"), (None, "Inconnu")], + null=True, + verbose_name="Accessibilité de l'aire de jeux à une personne en fauteuil", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_casiers", + field=models.BooleanField( + blank=True, + choices=[(True, "Oui"), (False, "Non"), (None, "Inconnu")], + null=True, + verbose_name="Présence de casiers", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_casiers_adaptes", + field=models.BooleanField( + blank=True, + choices=[(True, "Oui"), (False, "Non"), (None, "Inconnu")], + null=True, + verbose_name="Casiers adaptés", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_casiers_fermeture", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + blank=True, + choices=[ + ("serrure_avec_cle", "Serrure avec clé"), + ("serrure_electronique_a_code", "Serrure électronique à code"), + ("serrure_mecanique_a_code", "Serrure mécanique à code"), + ("verrou_cadenas", "Verrou pour cadenas personnel"), + ("autre", "Autre"), + ], + max_length=255, + ), + blank=True, + default=list, + null=True, + verbose_name="Système de fermeture des casiers", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_douches_collectives", + field=models.BooleanField( + blank=True, + choices=[(True, "Oui"), (False, "Non"), (None, "Inconnu")], + null=True, + verbose_name="Présence de douches collectives", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_douches_collectives_adaptees", + field=models.BooleanField( + blank=True, + choices=[(True, "Oui"), (False, "Non"), (None, "Inconnu")], + null=True, + verbose_name="Accessibilité des douches collectives", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_douches_individuelles", + field=models.BooleanField( + blank=True, + choices=[(True, "Oui"), (False, "Non"), (None, "Inconnu")], + null=True, + verbose_name="Présence de douches individuelles", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_douches_individuelles_adaptees", + field=models.BooleanField( + blank=True, + choices=[(True, "Oui"), (False, "Non"), (None, "Inconnu")], + null=True, + verbose_name="Accessibilité des douches individuelles", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_physique", + field=models.CharField( + blank=True, + choices=[ + ("inexistant", "Aucun personnel"), + ("non_forme", "Personnel non formé"), + ("sensibilise_ou_forme", "Personne sensibilisé ou formé"), + (None, "Inconnu"), + ], + max_length=25, + null=True, + verbose_name="Accueil physique", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_presence_espaces_specifiques", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + blank=True, + choices=[ + ( + "presence_espace_depose_equipements", + "Espace de dépose d'équipements (fauteuils, déambulateurs...)", + ), + ( + "presence_espace_repos_sensoriel", + "Espace de repos sensoriel (hyposensoriel, d'apaisement...)", + ), + ("presence_espace_chiens_guides", "Espace destiné aux chiens guides et d'assistance"), + ( + "presence_espace_salle_de_change", + "Salle de change destinée aux personnes ayant des handicaps multiples ou complexes", + ), + ], + max_length=255, + ), + blank=True, + default=list, + null=True, + verbose_name="Présence d’espaces spécifiques", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_prestations_complementaires", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + blank=True, + choices=[ + ("sonorisation_arbitrage", "Sonorisation de l'arbitrage possible"), + ("score_visible", "Score visible depuis les places assises"), + ( + "prestation_espace_depose_equipements", + "Espace de dépose d'équipements (fauteuils, déambulateurs...)", + ), + ("reservation_place_facile_d_acces", "Réservation possible de places facile d'accès"), + ], + max_length=255, + ), + blank=True, + default=list, + null=True, + verbose_name="Prestations complémentaires", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_tribunes", + field=models.BooleanField( + blank=True, + choices=[(True, "Oui"), (False, "Non"), (None, "Inconnu")], + null=True, + verbose_name="Accessibilité des places des tribunes", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_tribunes_localisation_places", + field=models.CharField( + blank=True, + choices=[ + ("niveau_aire_de_jeux", "Places situées au niveau de l'aire de jeux"), + ("reparti_differents_niveaux", "Places répartis différents niveaux des tribunes"), + (None, "Inconnu"), + ], + max_length=100, + null=True, + verbose_name="Localisation des places dans les tribunes", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_tribunes_places", + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + verbose_name="Nombre de places accessibles pour les personnes en fauteuil roulant", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_tribunes_places_avec_accompagnants", + field=models.PositiveSmallIntegerField( + blank=True, null=True, verbose_name="Nombre de places pour les accompagnants dans les tribunes" + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_vestiaires", + field=models.BooleanField( + blank=True, + choices=[(True, "Oui"), (False, "Non"), (None, "Inconnu")], + null=True, + verbose_name="Accessibilité des vestiaires", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="accueil_vestiaires_largeur_passage", + field=models.CharField( + blank=True, + choices=[ + ("entre_90_et_110", "Entre 90 et 110 cm"), + ("superieur_a_110", "Supérieure à 110 cm"), + (None, "Inconnu"), + ], + max_length=40, + null=True, + verbose_name="Largeur de passage pour se rendre dans les vestiaires", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="sanitaires_largeur_porte", + field=models.CharField( + blank=True, + choices=[ + ("entre_80_et_90", "Entre 80 et 90 cm"), + ("entre_90_et_110", "Entre 90 et 110 cm"), + (None, "Inconnu"), + ], + max_length=40, + null=True, + verbose_name="Largeur de porte des toilettes", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="sanitaires_sens_transfert", + field=models.CharField( + blank=True, + choices=[ + ("gauche", "Gauche"), + ("droite", "Droite"), + ("gauche_et_droite", "Gauche et droite"), + (None, "Inconnu"), + ], + max_length=40, + null=True, + verbose_name="Sens du transfert", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="sanitaires_urinoirs", + field=models.BooleanField( + blank=True, + choices=[(True, "Oui"), (False, "Non"), (None, "Inconnu")], + null=True, + verbose_name="Hauteur des urinoirs", + ), + ), + migrations.AddField( + model_name="accessibilite", + name="stationnement_zone_depose_pmr", + field=models.BooleanField( + blank=True, + choices=[(True, "Oui"), (False, "Non"), (None, "Inconnu")], + null=True, + verbose_name="Aire de dépose PMR", + ), + ), + migrations.AlterField( + model_name="accessibilite", + name="accueil_prise_en_charge_patients", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + blank=True, + choices=[ + ("preparation", "Préparation à la consultation ou visite préalable"), + ("organisation", "Organisation spécifique pour réduire le temps d’attente"), + ("outils_communication", "Outils de communication simplifiés pour expliquer les actes"), + ("materiel_auscultation", "Matériel médical adapté aux personnes obèses"), + ], + max_length=255, + ), + blank=True, + default=list, + null=True, + verbose_name="Prise en charge des patients", + ), + ), + migrations.AlterField( + model_name="accessibilite", + name="accueil_soignant_experience", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + blank=True, + choices=[ + ("visuel", "Visuel"), + ("auditif", "Auditif"), + ("moteur", "Moteur"), + ("mental", "Mental (déficience intellectuelle)"), + ("psychique", "Psychique (psychose, trouble bipolaire)"), + ("cognitif", "Cognitif (trouble du spectre de l'autisme, TDAH...)"), + ], + max_length=255, + ), + blank=True, + default=list, + null=True, + verbose_name="Expérience du soignant concernant les types de handicap", + ), + ), + migrations.AlterField( + model_name="erp", + name="checked_up_to_date_at", + field=models.DateTimeField( + auto_now_add=True, null=True, verbose_name="Dernière vérification des informations" + ), + ), + migrations.AddConstraint( + model_name="accessibilite", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("sanitaires_presence", True), ("sanitaires_presence__isnull", False)), + models.Q(("sanitaires_adaptes", None), ("sanitaires_urinoirs", None)), + _connector="OR", + ), + name="erp_accessibilite_sanitaires_presence_consistency", + ), + ), + migrations.AddConstraint( + model_name="accessibilite", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("sanitaires_adaptes", True), ("sanitaires_adaptes__isnull", False)), + models.Q( + models.Q( + ("sanitaires_largeur_porte__isnull", True), + ("sanitaires_largeur_porte", ""), + _connector="OR", + ), + models.Q( + ("sanitaires_sens_transfert__isnull", True), + ("sanitaires_sens_transfert", ""), + _connector="OR", + ), + ), + _connector="OR", + ), + name="erp_accessibilite_sanitaires_adaptes_consistency", + ), + ), + migrations.AddConstraint( + model_name="accessibilite", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("accueil_casiers", True), ("accueil_casiers__isnull", False)), + models.Q( + ("accueil_casiers_adaptes", None), + models.Q( + ("accueil_casiers_fermeture__isnull", True), + ("accueil_casiers_fermeture", []), + _connector="OR", + ), + ), + _connector="OR", + ), + name="erp_accessibilite_accueil_casiers_consistency", + ), + ), + migrations.AddConstraint( + model_name="accessibilite", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q( + ("accueil_tribunes_places", 0), + ("accueil_tribunes_places__isnull", True), + _connector="OR", + _negated=True, + ), + models.Q( + models.Q( + ("accueil_tribunes_localisation_places__isnull", True), + ("accueil_tribunes_localisation_places", ""), + _connector="OR", + ), + ("accueil_tribunes_places_avec_accompagnants__isnull", True), + ), + _connector="OR", + ), + name="erp_accessibilite_accueil_tribunes_places_consistency", + ), + ), + migrations.AddConstraint( + model_name="accessibilite", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("accueil_vestiaires", True), ("accueil_vestiaires__isnull", False)), + ("accueil_vestiaires_largeur_passage__isnull", True), + ("accueil_vestiaires_largeur_passage", ""), + _connector="OR", + ), + name="erp_accessibilite_accueil_vestiaires_consistency", + ), + ), + ] diff --git a/erp/models.py b/erp/models.py index 901cfcc16b..3151323599 100644 --- a/erp/models.py +++ b/erp/models.py @@ -44,6 +44,7 @@ "LARGE_ESTABLISHMENTS": "Grands établissements", "POLLING_STATION": "Bureau de vote", "HEALTHCARE": "Santé", + "SPORTS_EQUIPMENT": "Équipements sportifs", } @@ -1265,7 +1266,15 @@ class Meta: CheckConstraint( name="%(app_label)s_%(class)s_sanitaires_presence_consistency", condition=(Q(sanitaires_presence=True) & Q(sanitaires_presence__isnull=False)) - | Q(sanitaires_adaptes=None), + | (Q(sanitaires_adaptes=None) & Q(sanitaires_urinoirs=None)), + ), + CheckConstraint( + name="%(app_label)s_%(class)s_sanitaires_adaptes_consistency", + condition=(Q(sanitaires_adaptes=True) & Q(sanitaires_adaptes__isnull=False)) + | ( + (Q(sanitaires_largeur_porte__isnull=True) | Q(sanitaires_largeur_porte="")) + & (Q(sanitaires_sens_transfert__isnull=True) | Q(sanitaires_sens_transfert="")) + ), ), CheckConstraint( name="%(app_label)s_%(class)s_labels_consistency", @@ -1297,6 +1306,36 @@ class Meta: | (Q(accueil_soignant_experience__isnull=True) | Q(accueil_soignant_experience=[])) ), ), + CheckConstraint( + name="%(app_label)s_%(class)s_accueil_casiers_consistency", + condition=( + (Q(accueil_casiers=True) & Q(accueil_casiers__isnull=False)) + | ( + Q(accueil_casiers_adaptes=None) + & (Q(accueil_casiers_fermeture__isnull=True) | Q(accueil_casiers_fermeture=[])) + ) + ), + ), + CheckConstraint( + name="%(app_label)s_%(class)s_accueil_tribunes_places_consistency", + condition=( + ~(Q(accueil_tribunes_places=0) | Q(accueil_tribunes_places__isnull=True)) + | ( + ( + Q(accueil_tribunes_localisation_places__isnull=True) + | Q(accueil_tribunes_localisation_places="") + ) + & Q(accueil_tribunes_places_avec_accompagnants__isnull=True) + ) + ), + ), + CheckConstraint( + name="%(app_label)s_%(class)s_accueil_vestiaires_consistency", + condition=( + (Q(accueil_vestiaires=True) & Q(accueil_vestiaires__isnull=False)) + | (Q(accueil_vestiaires_largeur_passage__isnull=True) | Q(accueil_vestiaires_largeur_passage="")) + ), + ), ] erp = models.OneToOneField( @@ -1361,6 +1400,12 @@ class Meta: choices=schema.get_field_choices("stationnement_ext_pmr"), verbose_name=translate_lazy("Stationnements PMR à proximité de l'ERP"), ) + stationnement_zone_depose_pmr = models.BooleanField( + null=True, + blank=True, + choices=schema.get_field_choices("stationnement_zone_depose_pmr"), + verbose_name=translate_lazy("Aire de dépose PMR"), + ) ################################### # Espace et Cheminement extérieur # @@ -1897,6 +1942,112 @@ class Meta: blank=True, verbose_name=translate_lazy("Prise en charge des patients"), ) + accueil_physique = models.CharField( + max_length=25, + null=True, + blank=True, + verbose_name=translate_lazy("Accueil physique"), + choices=schema.ACCUEIL_PHYSIQUE_CHOICES, + ) + accueil_aire_de_jeux = models.BooleanField( + null=True, + blank=True, + choices=schema.get_field_choices("accueil_aire_de_jeux"), + verbose_name=translate_lazy("Accessibilité de l'aire de jeux à une personne en fauteuil"), + ) + accueil_tribunes = models.BooleanField( + null=True, + blank=True, + choices=schema.get_field_choices("accueil_tribunes"), + verbose_name=translate_lazy("Accessibilité des places des tribunes"), + ) + accueil_tribunes_places = models.PositiveSmallIntegerField( + null=True, + blank=True, + verbose_name=translate_lazy("Nombre de places accessibles pour les personnes en fauteuil roulant"), + ) + accueil_tribunes_localisation_places = models.CharField( + max_length=100, + null=True, + blank=True, + verbose_name=translate_lazy("Localisation des places dans les tribunes"), + choices=schema.ACCUEIL_TRIBUNES_PLACES_CHOICES, + ) + accueil_tribunes_places_avec_accompagnants = models.PositiveSmallIntegerField( + null=True, + blank=True, + verbose_name=translate_lazy("Nombre de places pour les accompagnants dans les tribunes"), + ) + accueil_vestiaires = models.BooleanField( + null=True, + blank=True, + choices=schema.get_field_choices("accueil_vestiaires"), + verbose_name=translate_lazy("Accessibilité des vestiaires"), + ) + accueil_vestiaires_largeur_passage = models.CharField( + max_length=40, + null=True, + blank=True, + verbose_name=translate_lazy("Largeur de passage pour se rendre dans les vestiaires"), + choices=schema.ACCUEIL_VESTIAIRES_LARGEUR_PASSAGE_CHOICES, + ) + accueil_douches_collectives = models.BooleanField( + null=True, + blank=True, + choices=schema.get_field_choices("accueil_douches_collectives"), + verbose_name=translate_lazy("Présence de douches collectives"), + ) + accueil_douches_collectives_adaptees = models.BooleanField( + null=True, + blank=True, + choices=schema.get_field_choices("accueil_douches_collectives_adaptees"), + verbose_name=translate_lazy("Accessibilité des douches collectives"), + ) + accueil_douches_individuelles = models.BooleanField( + null=True, + blank=True, + choices=schema.get_field_choices("accueil_douches_individuelles"), + verbose_name=translate_lazy("Présence de douches individuelles"), + ) + accueil_douches_individuelles_adaptees = models.BooleanField( + null=True, + blank=True, + choices=schema.get_field_choices("accueil_douches_individuelles_adaptees"), + verbose_name=translate_lazy("Accessibilité des douches individuelles"), + ) + accueil_casiers = models.BooleanField( + null=True, + blank=True, + choices=schema.get_field_choices("accueil_casiers"), + verbose_name=translate_lazy("Présence de casiers"), + ) + accueil_casiers_adaptes = models.BooleanField( + null=True, + blank=True, + choices=schema.get_field_choices("accueil_casiers_adaptes"), + verbose_name=translate_lazy("Casiers adaptés"), + ) + accueil_casiers_fermeture = ArrayField( + models.CharField(max_length=255, blank=True, choices=schema.ACCUEIL_SYSTEME_FERMETURE_CHOICES), + default=list, + null=True, + blank=True, + verbose_name=translate_lazy("Système de fermeture des casiers"), + ) + accueil_prestations_complementaires = ArrayField( + models.CharField(max_length=255, blank=True, choices=schema.ACCUEIL_PRESTATIONS_COMPLEMENTAIRES_CHOICES), + verbose_name=translate_lazy("Prestations complémentaires"), + default=list, + null=True, + blank=True, + ) + accueil_presence_espaces_specifiques = ArrayField( + models.CharField(max_length=255, blank=True, choices=schema.ACCUEIL_PRESENCE_ESPACES_SPECIFIQUES_CHOICES), + verbose_name=translate_lazy("Présence d’espaces spécifiques"), + default=list, + null=True, + blank=True, + ) ############## # Sanitaires # ############## @@ -1906,14 +2057,32 @@ class Meta: choices=schema.get_field_choices("sanitaires_presence"), verbose_name=translate_lazy("Sanitaires"), ) - sanitaires_adaptes = models.BooleanField( null=True, blank=True, choices=schema.get_field_choices("sanitaires_adaptes"), verbose_name=translate_lazy("Nombre de sanitaires adaptés"), ) - + sanitaires_largeur_porte = models.CharField( + max_length=40, + null=True, + blank=True, + verbose_name=translate_lazy("Largeur de porte des toilettes"), + choices=schema.SANITAIRES_LARGEUR_PORTE_CHOICES, + ) + sanitaires_sens_transfert = models.CharField( + max_length=40, + null=True, + blank=True, + verbose_name=translate_lazy("Sens du transfert"), + choices=schema.SANITAIRES_SENS_TRANSFERT_CHOICES, + ) + sanitaires_urinoirs = models.BooleanField( + null=True, + blank=True, + verbose_name=translate_lazy("Hauteur des urinoirs"), + choices=schema.get_field_choices("sanitaires_urinoirs"), + ) ########## # labels # ########## @@ -2138,6 +2307,51 @@ def get_accueil_soignant_experience(self): for soignant_experience in self.accueil_soignant_experience ] + def get_accueil_tribunes_places_avec_accompagnants(self): + field = schema.FIELDS["accueil_tribunes_places_avec_accompagnants"] + if not self.accueil_tribunes_places_avec_accompagnants: + return str(field["help_text_ui_neg"]) + + return "{} : {}".format(field["help_text_ui"], self.accueil_tribunes_places_avec_accompagnants) + + def get_sanitaires_largeur_porte(self): + text = schema.FIELDS["sanitaires_largeur_porte"]["help_text_ui"] + " " + self.sanitaires_largeur_porte + return text + + def get_accueil_tribunes_places(self): + return "{} {}".format(self.accueil_tribunes_places, schema.FIELDS["accueil_tribunes_places"]["help_text_ui"]) + + def get_accueil_casiers_fermeture(self): + if not self.accueil_casiers_fermeture: + return + accueil_casiers_fermeture_text = {k: str(v) for k, v in schema.ACCUEIL_SYSTEME_FERMETURE_CHOICES} + return [ + accueil_casiers_fermeture_text.get(casiers_fermeture) + for casiers_fermeture in self.accueil_casiers_fermeture + ] + + def get_accueil_prestations_complementaires(self): + if not self.accueil_prestations_complementaires: + return + accueil_prestations_complementaires_text = { + k: str(v) for k, v in schema.ACCUEIL_PRESTATIONS_COMPLEMENTAIRES_CHOICES + } + return [ + accueil_prestations_complementaires_text.get(prestations_complementaires) + for prestations_complementaires in self.accueil_prestations_complementaires + ] + + def get_accueil_presence_espaces_specifiques(self): + if not self.accueil_presence_espaces_specifiques: + return + accueil_presence_espaces_specifiques_text = { + k: str(v) for k, v in schema.ACCUEIL_PRESENCE_ESPACES_SPECIFIQUES_CHOICES + } + return [ + accueil_presence_espaces_specifiques_text.get(presence_espaces_specifiques) + for presence_espaces_specifiques in self.accueil_presence_espaces_specifiques + ] + def get_accueil_prise_en_charge_patients(self): if not self.accueil_prise_en_charge_patients: return diff --git a/erp/schema.py b/erp/schema.py index 29de896f69..2a9693762a 100644 --- a/erp/schema.py +++ b/erp/schema.py @@ -151,6 +151,106 @@ ), ] +ACCUEIL_PHYSIQUE_INEXISTANT = "inexistant" +ACCUEIL_PHYSIQUE_NON_FORME = "non_forme" +ACCUEIL_PHYSIQUE_SENSIBILISE_OU_FORME = "sensibilise_ou_forme" +ACCUEIL_PHYSIQUE_CHOICES = [ + (ACCUEIL_PHYSIQUE_INEXISTANT, translate_lazy("Aucun personnel")), + (ACCUEIL_PHYSIQUE_NON_FORME, translate_lazy("Personnel non formé")), + (ACCUEIL_PHYSIQUE_SENSIBILISE_OU_FORME, translate_lazy("Personne sensibilisé ou formé")), + (None, UNKNOWN), +] + +ACCUEIL_TRIBUNES_PLACES_AIRE_DE_JEUX = "niveau_aire_de_jeux" +ACCUEIL_TRIBUNES_PLACES_REPARTIES_DIFFERENTS_NIVEAUX = "reparti_differents_niveaux" +ACCUEIL_TRIBUNES_PLACES_CHOICES = [ + (ACCUEIL_TRIBUNES_PLACES_AIRE_DE_JEUX, translate_lazy("Places situées au niveau de l'aire de jeux")), + ( + ACCUEIL_TRIBUNES_PLACES_REPARTIES_DIFFERENTS_NIVEAUX, + translate_lazy("Places répartis différents niveaux des tribunes"), + ), + (None, UNKNOWN), +] + +ACCUEIL_VESTIAIRES_LARGEUR_PASSAGE_ENTRE_90_ET_110 = "entre_90_et_110" +ACCUEIL_VESTIAIRES_LARGEUR_PASSAGE_SUPERIEUR_A_110 = "superieur_a_110" +ACCUEIL_VESTIAIRES_LARGEUR_PASSAGE_CHOICES = [ + (ACCUEIL_VESTIAIRES_LARGEUR_PASSAGE_ENTRE_90_ET_110, translate_lazy("Entre 90 et 110 cm")), + (ACCUEIL_VESTIAIRES_LARGEUR_PASSAGE_SUPERIEUR_A_110, translate_lazy("Supérieure à 110 cm")), + (None, UNKNOWN), +] + +SANITAIRES_LARGEUR_PORTE_ENTRE_80_et_90 = "entre_80_et_90" +SANITAIRES_LARGEUR_PORTE_ENTRE_90_ET_110 = "entre_90_et_110" +SANITAIRES_LARGEUR_PORTE_CHOICES = [ + (SANITAIRES_LARGEUR_PORTE_ENTRE_80_et_90, translate_lazy("Entre 80 et 90 cm")), + (SANITAIRES_LARGEUR_PORTE_ENTRE_90_ET_110, translate_lazy("Entre 90 et 110 cm")), + (None, UNKNOWN), +] + +SANITAIRES_SENS_TRANSFERT_GAUCHE = "gauche" +SANITAIRES_SENS_TRANSFERT_DROITE = "droite" +SANITAIRES_SENS_TRANSFERT_GAUCHE_ET_DROITE = "gauche_et_droite" +SANITAIRES_SENS_TRANSFERT_CHOICES = [ + (SANITAIRES_SENS_TRANSFERT_GAUCHE, translate_lazy("Gauche")), + (SANITAIRES_SENS_TRANSFERT_DROITE, translate_lazy("Droite")), + (SANITAIRES_SENS_TRANSFERT_GAUCHE_ET_DROITE, translate_lazy("Gauche et droite")), + (None, UNKNOWN), +] + +ACCUEIL_SYSTEME_FERMETURE_SERRURE_AVEC_CLE = "serrure_avec_cle" +ACCUEIL_SYSTEME_FERMETURE_ELECTRONIQUE_A_CODE = "serrure_electronique_a_code" +ACCUEIL_SYSTEME_FERMETURE_MECANIQUE_A_CODE = "serrure_mecanique_a_code" +ACCUEIL_SYSTEME_FERMETURE_VERROU_CADENAS = "verrou_cadenas" +ACCUEIL_SYSTEME_FERMETURE_AUTRE = "autre" +ACCUEIL_SYSTEME_FERMETURE_CHOICES = [ + (ACCUEIL_SYSTEME_FERMETURE_SERRURE_AVEC_CLE, translate_lazy("Serrure avec clé")), + (ACCUEIL_SYSTEME_FERMETURE_ELECTRONIQUE_A_CODE, translate_lazy("Serrure électronique à code")), + (ACCUEIL_SYSTEME_FERMETURE_MECANIQUE_A_CODE, translate_lazy("Serrure mécanique à code")), + (ACCUEIL_SYSTEME_FERMETURE_VERROU_CADENAS, translate_lazy("Verrou pour cadenas personnel")), + (ACCUEIL_SYSTEME_FERMETURE_AUTRE, translate_lazy("Autre")), +] + +ACCUEIL_PRESTATIONS_COMPLEMENTAIRES_SONORISATION = "sonorisation_arbitrage" +ACCUEIL_PRESTATIONS_COMPLEMENTAIRES_SCORE_VISIBLE = "score_visible" +ACCUEIL_PRESTATIONS_COMPLEMENTAIRES_ESPACE_DEPOSE_EQUIPEMENTS = "prestation_espace_depose_equipements" +ACCUEIL_PRESTATIONS_COMPLEMENTAIRES_RESERVATION_PLACE_FACILE_ACCES = "reservation_place_facile_d_acces" +ACCUEIL_PRESTATIONS_COMPLEMENTAIRES_CHOICES = [ + (ACCUEIL_PRESTATIONS_COMPLEMENTAIRES_SONORISATION, translate_lazy("Sonorisation de l'arbitrage possible")), + (ACCUEIL_PRESTATIONS_COMPLEMENTAIRES_SCORE_VISIBLE, translate_lazy("Score visible depuis les places assises")), + ( + ACCUEIL_PRESTATIONS_COMPLEMENTAIRES_ESPACE_DEPOSE_EQUIPEMENTS, + translate_lazy("Espace de dépose d'équipements (fauteuils, déambulateurs...)"), + ), + ( + ACCUEIL_PRESTATIONS_COMPLEMENTAIRES_RESERVATION_PLACE_FACILE_ACCES, + translate_lazy("Réservation possible de places facile d'accès"), + ), +] + +ACCUEIL_PRESENCE_ESPACES_SPECIFIQUES_DEPOSE_EQUIPEMENTS = "presence_espace_depose_equipements" +ACCUEIL_PRESENCE_ESPACES_SPECIFIQUES_REPOS_SENSORIEL = "presence_espace_repos_sensoriel" +ACCUEIL_PRESENCE_ESPACES_SPECIFIQUES_CHIENS_GUIDES = "presence_espace_chiens_guides" +ACCUEIL_PRESENCE_ESPACES_SPECIFIQUES_SALLE_DE_CHANGE = "presence_espace_salle_de_change" +ACCUEIL_PRESENCE_ESPACES_SPECIFIQUES_CHOICES = [ + ( + ACCUEIL_PRESENCE_ESPACES_SPECIFIQUES_DEPOSE_EQUIPEMENTS, + translate_lazy("Espace de dépose d'équipements (fauteuils, déambulateurs...)"), + ), + ( + ACCUEIL_PRESENCE_ESPACES_SPECIFIQUES_REPOS_SENSORIEL, + translate_lazy("Espace de repos sensoriel (hyposensoriel, d'apaisement...)"), + ), + ( + ACCUEIL_PRESENCE_ESPACES_SPECIFIQUES_CHIENS_GUIDES, + translate_lazy("Espace destiné aux chiens guides et d'assistance"), + ), + ( + ACCUEIL_PRESENCE_ESPACES_SPECIFIQUES_SALLE_DE_CHANGE, + translate_lazy("Salle de change destinée aux personnes ayant des handicaps multiples ou complexes"), + ), +] + HANDICAP_AUDITIF = "auditif" HANDICAP_MENTAL = "mental" HANDICAP_MOTEUR = "moteur" @@ -963,6 +1063,33 @@ "free_text": False, "root": False, }, + "stationnement_zone_depose_pmr": { + "type": "boolean", + "nullable": True, + "is_a11y": True, + "label": translate_lazy("Aire de dépose PMR"), + "help_text": mark_safe( + translate_lazy( + "Existe-t-il une zone de dépose PMR pour les grands véhicules adaptés à proximité de l'entrée du gymnase ? (au moins 8m de longueur)" + ) + ), + "help_text_ui": translate_lazy("Présence d'une zone de dépose PMR"), + "help_text_ui_v2": translate_lazy("Présence d'une zone de dépose PMR"), + "help_text_ui_neg": translate_lazy("Pas de zone de dépose PMR"), + "help_text_ui_neg_v2": translate_lazy("Pas de zone de dépose PMR"), + "choices": NULLABLE_BOOLEAN_CHOICES, + "choices_images": ( + ("/static/img/contrib/zone-depose-pmr.png"), + ("/static/img/contrib/no.png"), + ("/static/img/contrib/unknown.png"), + ), + "conditional": "sports_equipment", + "section": SECTION_TRANSPORT, + "nullable_bool": True, + "warn_if": False, + "free_text": False, + "root": True, + }, # Cheminement extérieur "cheminement_ext_presence": { "type": "boolean", @@ -2688,6 +2815,320 @@ "root": True, "excluded_from_completion_rate": True, }, + "accueil_physique": { + "type": "string", + "nullable": True, + "is_a11y": True, + "label": translate_lazy("Accueil physique"), + "help_text": mark_safe( + translate_lazy("Y a-t-il du personnel présent pendant les heures d’ouverture de l'établissement ?") + ), + "help_text_ui": translate_lazy("Présence de personnel pendant les heures d'ouverture"), + "help_text_ui_neg": translate_lazy("Pas de personnel dans l'établisement"), + "choices": ACCUEIL_PHYSIQUE_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "warn_if": None, + "free_text": False, + "conditional": "sports_equipment", + "root": True, + }, + "accueil_aire_de_jeux": { + "type": "boolean", + "nullable": True, + "is_a11y": True, + "label": translate_lazy("Aire de jeux"), + "help_text": mark_safe(translate_lazy("L'aire de jeux est-elle accessible à une personne en fauteuil ?")), + "help_text_ui": translate_lazy("L'aire de jeux est accessible à une personne en fauteuil"), + "help_text_ui_neg": translate_lazy("L'aire de jeux n'est pas accessible à une personne en fauteuil"), + "choices": NULLABLE_BOOLEAN_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "warn_if": False, + "free_text": False, + "conditional": "sports_equipment", + "root": True, + }, + "accueil_tribunes": { + "type": "boolean", + "nullable": True, + "is_a11y": True, + "label": translate_lazy("Tribunes"), + "help_text": mark_safe(translate_lazy("Les places destinées aux spectateurs ont-elles un dossier ?")), + "help_text_ui": translate_lazy("Places spectateurs avec dossier"), + "help_text_ui_neg": translate_lazy("Places spectateurs sans dossier"), + "choices": NULLABLE_BOOLEAN_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "warn_if": False, + "free_text": False, + "conditional": "sports_equipment", + "root": True, + }, + "accueil_tribunes_places": { + "type": "number", + "nullable": True, + "is_a11y": True, + "label": translate_lazy("Places accessibles en tribunes"), + "help_text": mark_safe( + translate_lazy("Combien y a-t-il de places accessibles pour les personnes en fauteuil roulant ?") + ), + "help_text_ui": translate_lazy("Places accessibles pour les personnes en fauteuil roulant"), + "help_text_ui_neg": translate_lazy("Places accessibles pour les personnes en fauteuil roulant"), + "choices": None, + "unit": "place", + "section": SECTION_ACCUEIL, + "nullable_bool": False, + "free_text": False, + "should_display_label": False, + "conditional": "sports_equipment", + "min_value_to_display_children": 1, + "children": ["accueil_tribunes_localisation_places", "accueil_tribunes_places_avec_accompagnants"], + "root": True, + }, + "accueil_tribunes_localisation_places": { + "type": "string", + "nullable": True, + "is_a11y": True, + "help_text": mark_safe(translate_lazy("Où sont elles localisées ?")), + "help_text_ui": translate_lazy("Localisation des places"), + "help_text_ui_neg": translate_lazy("Localisation des places"), + "choices": ACCUEIL_TRIBUNES_PLACES_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "free_text": False, + "conditional": "sports_equipment", + "should_display_label": False, + "root": False, + }, + "accueil_tribunes_places_avec_accompagnants": { + "type": "number", + "nullable": True, + "is_a11y": True, + "help_text": mark_safe( + translate_lazy( + "Est-ce que des places pour les accompagnants sont proposées à côté des places pour les personnes en fauteuil roulant ?" + ) + ), + "help_text_ui": translate_lazy("Places accompagnant à côté des places pour les personnes en fauteuil roulant"), + "help_text_ui_neg": translate_lazy( + "Pas de place accompagnant à côté des places pour les personnes en fauteuil roulant" + ), + "choices": None, + "unit": "place", + "section": SECTION_ACCUEIL, + "nullable_bool": False, + "free_text": False, + "conditional": "sports_equipment", + "should_display_label": False, + "root": False, + }, + "accueil_vestiaires": { + "type": "boolean", + "nullable": True, + "is_a11y": True, + "label": translate_lazy("Vestiaires"), + "help_text": mark_safe( + translate_lazy( + "Les vestiaires sont-ils accessibles à une personne en fauteuil roulant (cheminement et porte) ?" + ) + ), + "help_text_ui": translate_lazy("Vestiaires accessibles"), + "help_text_ui_neg": translate_lazy( + "Les vestiaires ne sont pas accessibles aux personnes en fauteuil roulant par un passage d'une largeur entre 90 et 110 cm/ supérieure à 110 cm" + ), + "choices": NULLABLE_BOOLEAN_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "warn_if": False, + "free_text": False, + "conditional": "sports_equipment", + "value_to_display_children": ["True"], + "children": ["accueil_vestiaires_largeur_passage"], + "root": True, + }, + "accueil_vestiaires_largeur_passage": { + "type": "string", + "nullable": True, + "is_a11y": True, + "help_text": mark_safe(translate_lazy("Quelle est la largeur de passage pour se rendre dans les vestiaires ?")), + "help_text_ui": translate_lazy("Largeur du passage d'au moins 90 cm"), + "help_text_ui_neg": translate_lazy("Largeur inférieure à 90 cm"), + "choices": ACCUEIL_VESTIAIRES_LARGEUR_PASSAGE_CHOICES, + "unit": "centimètre", + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "free_text": False, + "conditional": "sports_equipment", + "should_display_label": False, + "root": False, + }, + "accueil_douches_collectives": { + "type": "boolean", + "nullable": True, + "is_a11y": True, + "label": translate_lazy("Douches collectives"), + "help_text": mark_safe(translate_lazy("Présence de douches collectives ?")), + "help_text_ui": translate_lazy("Présence de douches collectives"), + "help_text_ui_neg": translate_lazy("Pas de douches collectives"), + "choices": NULLABLE_BOOLEAN_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "warn_if": False, + "free_text": False, + "conditional": "sports_equipment", + "value_to_display_children": ["True"], + "children": ["accueil_douches_collectives_adaptees"], + "root": True, + }, + "accueil_douches_collectives_adaptees": { + "type": "boolean", + "nullable": True, + "is_a11y": True, + "help_text": mark_safe( + translate_lazy( + "Les douches sont-elles accessibles pour une personne en fauteuil roulant ? (accès de plain pied, largeur minimale de 90cm, espace de giration)" + ) + ), + "help_text_ui": translate_lazy("Présence de douches collectives accessibles"), + "help_text_ui_neg": translate_lazy("Pas de douches collectives accessibles aux personnes en fauteuil roulant"), + "choices": NULLABLE_BOOLEAN_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "warn_if": False, + "free_text": False, + "conditional": "sports_equipment", + "should_display_label": False, + "root": False, + }, + "accueil_douches_individuelles": { + "type": "boolean", + "label": translate_lazy("Douches individuelles"), + "nullable": True, + "is_a11y": True, + "help_text": mark_safe(translate_lazy("Présence de cabines de douche individuelles ?")), + "help_text_ui": translate_lazy("Présence de cabines de douche individuelles accessibles"), + "help_text_ui_neg": translate_lazy( + "Pas de cabine de douche individuelles accessibles aux personnes en fauteuil roulant" + ), + "choices": NULLABLE_BOOLEAN_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "warn_if": False, + "free_text": False, + "conditional": "sports_equipment", + "value_to_display_children": ["True"], + "children": ["accueil_douches_individuelles_adaptees"], + "root": True, + }, + "accueil_douches_individuelles_adaptees": { + "type": "boolean", + "nullable": True, + "is_a11y": True, + "help_text": mark_safe( + translate_lazy( + "Est-ce qu'il y a des cabines de douches accessibles ? (Espace de giration, diamètre de 1m50, espace d'usage, espace derrière la porte.)" + ) + ), + "help_text_ui": translate_lazy("Présence de cabines de douche individuelles accessibles"), + "help_text_ui_neg": translate_lazy( + "Pas de cabines de douche individuelles accessibles aux personnes en fauteuil roulant" + ), + "choices": NULLABLE_BOOLEAN_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "warn_if": False, + "free_text": False, + "conditional": "sports_equipment", + "should_display_label": False, + "root": False, + }, + "accueil_casiers": { + "type": "boolean", + "nullable": True, + "is_a11y": True, + "label": translate_lazy("Casiers"), + "help_text": mark_safe(translate_lazy("Y a-t-il des casiers mis à disposition du public?")), + "help_text_ui": translate_lazy("Casiers à disposition du public"), + "help_text_ui_neg": translate_lazy("Pas de casiers à disposition du public"), + "choices": NULLABLE_BOOLEAN_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "warn_if": False, + "free_text": False, + "conditional": "sports_equipment", + "value_to_display_children": ["True"], + "children": ["accueil_casiers_adaptes", "accueil_casiers_fermeture"], + "root": True, + }, + "accueil_casiers_adaptes": { + "type": "boolean", + "nullable": True, + "is_a11y": True, + "help_text": mark_safe( + translate_lazy( + "Le contraste et/ou le relief des casiers et de leur numérotation permettent-ils une bonne lisibilité pour une personne malvoyante ?" + ) + ), + "help_text_ui": translate_lazy("Le contraste et le relief des casiers permettent une bonne lisibilité."), + "help_text_ui_neg": translate_lazy( + "Le contraste et le relief des casiers ne permettent pas une bonne lisibilité." + ), + "choices": NULLABLE_BOOLEAN_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "warn_if": False, + "free_text": False, + "conditional": "sports_equipment", + "should_display_label": False, + "root": False, + }, + "accueil_casiers_fermeture": { + "type": "array", + "nullable": True, + "is_a11y": True, + "help_text": mark_safe(translate_lazy("Quel est leur système de fermeture ?")), + "help_text_ui": translate_lazy("Système de fermeture présent"), + "help_text_ui_neg": translate_lazy("Système de fermeture absent"), + "choices": ACCUEIL_SYSTEME_FERMETURE_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": False, + "warn_if": False, + "conditional": "sports_equipment", + "should_display_label": False, + "free_text": False, + "root": False, + }, + "accueil_prestations_complementaires": { + "type": "array", + "nullable": True, + "is_a11y": True, + "label": mark_safe(translate_lazy("Prestations complémentaires")), + "help_text_ui": translate_lazy("Prestations complémentaires disponibles"), + "help_text_ui_neg": translate_lazy("Pas de prestations complémentaires"), + "choices": ACCUEIL_PRESTATIONS_COMPLEMENTAIRES_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": False, + "warn_if": False, + "conditional": "sports_equipment", + "free_text": False, + "root": True, + }, + "accueil_presence_espaces_specifiques": { + "type": "array", + "nullable": True, + "is_a11y": True, + "label": mark_safe(translate_lazy("Présence d'espaces spécifiques")), + "help_text_ui": translate_lazy("Présence d'espaces spécifiques"), + "help_text_ui_neg": translate_lazy("Pas d'espaces spécifiques à disposition"), + "choices": ACCUEIL_PRESENCE_ESPACES_SPECIFIQUES_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": False, + "warn_if": False, + "conditional": "sports_equipment", + "free_text": False, + "root": True, + }, # Sanitaires "sanitaires_presence": { "type": "boolean", @@ -2705,7 +3146,7 @@ "free_text": False, "root": True, "value_to_display_children": ["True"], - "children": ["sanitaires_adaptes"], + "children": ["sanitaires_adaptes", "sanitaires_urinoirs"], }, "sanitaires_adaptes": { "type": "boolean", @@ -2726,6 +3167,56 @@ "nullable_bool": True, "warn_if": False, "free_text": False, + "value_to_display_children": ["True"], + "children": ["sanitaires_largeur_porte", "sanitaires_sens_transfert"], + }, + "sanitaires_largeur_porte": { + "type": "string", + "nullable": True, + "is_a11y": True, + "help_text": mark_safe(translate_lazy("Quelle est la largeur de porte des toilettes ?")), + "help_text_ui": translate_lazy("Toilettes adaptées avec une largeur de passage entre"), + "help_text_ui_neg": translate_lazy("Toilettes adaptées avec une largeur de passage entre"), + "choices": SANITAIRES_LARGEUR_PORTE_CHOICES, + "unit": "centimètre", + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "free_text": False, + "conditional": "sports_equipment", + "should_display_label": False, + "root": False, + }, + "sanitaires_sens_transfert": { + "type": "string", + "nullable": True, + "is_a11y": True, + "help_text": mark_safe(translate_lazy("Quel est le sens du transfert ?")), + "help_text_ui": translate_lazy("Sens du transfert"), + "help_text_ui_neg": translate_lazy("Sens du transfert"), + "choices": SANITAIRES_SENS_TRANSFERT_CHOICES, + "unit": "centimètre", + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "free_text": False, + "conditional": "sports_equipment", + "should_display_label": False, + "root": False, + }, + "sanitaires_urinoirs": { + "type": "boolean", + "nullable": True, + "is_a11y": True, + "help_text": translate_lazy("Est ce que les urinoirs sont positionnés à différentes hauteurs ?"), + "help_text_ui": translate_lazy("Urinoirs à différentes hauteurs"), + "help_text_ui_neg": translate_lazy("Les urinoirs sont tous à la même hauteur"), + "choices": NULLABLE_BOOLEAN_CHOICES, + "section": SECTION_ACCUEIL, + "nullable_bool": True, + "warn_if": False, + "free_text": False, + "conditional": "sports_equipment", + "should_display_label": False, + "root": False, }, # Labels "labels": { diff --git a/static/img/contrib/tribunes-places.png b/static/img/contrib/tribunes-places.png new file mode 100644 index 0000000000..3e636ecd2e Binary files /dev/null and b/static/img/contrib/tribunes-places.png differ diff --git a/static/img/contrib/zone-depose-pmr.png b/static/img/contrib/zone-depose-pmr.png new file mode 100644 index 0000000000..fe9a5d585d Binary files /dev/null and b/static/img/contrib/zone-depose-pmr.png differ diff --git a/static/js/ui/ConditionalForm.js b/static/js/ui/ConditionalForm.js index a8bf76c664..d4e15bf0ee 100644 --- a/static/js/ui/ConditionalForm.js +++ b/static/js/ui/ConditionalForm.js @@ -1,8 +1,3 @@ -const INDENTS_LEVEL = { - first: 6, - second: 10, -} - const rules = [ // Transport { @@ -15,13 +10,11 @@ const rules = [ source: 'stationnement_presence', values: ['True'], targets: ['stationnement_pmr'], - indent: INDENTS_LEVEL.first, }, { source: 'stationnement_ext_presence', values: ['True'], targets: ['stationnement_ext_pmr'], - indent: INDENTS_LEVEL.first, }, // Presence d'un extérieur et cheminement { @@ -36,7 +29,6 @@ const rules = [ 'cheminement_ext_retrecissement', 'cheminement_ext_pente_presence', ], - indent: INDENTS_LEVEL.first, }, { source: 'cheminement_ext_plain_pied', @@ -49,38 +41,32 @@ const rules = [ 'cheminement_ext_rampe', 'cheminement_ext_ascenseur', ], - indent: INDENTS_LEVEL.second, }, { source: 'cheminement_ext_ascenseur', values: ['True'], targets: ['cheminement_ext_ascenseur_pmr'], - indent: INDENTS_LEVEL.second, }, { source: 'cheminement_ext_pente_presence', values: ['True'], targets: ['cheminement_ext_pente_degre_difficulte', 'cheminement_ext_pente_longueur'], - indent: INDENTS_LEVEL.second, }, // Entrée { source: 'entree_porte_presence', values: ['True'], targets: ['entree_porte_manoeuvre', 'entree_porte_type', 'entree_vitree'], - indent: INDENTS_LEVEL.first, }, { source: 'entree_vitree', values: ['True'], targets: ['entree_vitree_vitrophanie'], - indent: INDENTS_LEVEL.second, }, { source: 'entree_dispositif_appel', values: ['True'], targets: ['entree_dispositif_appel_type'], - indent: INDENTS_LEVEL.first, }, { source: 'entree_plain_pied', @@ -93,32 +79,27 @@ const rules = [ 'entree_marches_rampe', 'entree_ascenseur', ], - indent: INDENTS_LEVEL.first, }, { source: 'entree_ascenseur', values: ['True'], targets: ['entree_ascenseur_pmr'], - indent: INDENTS_LEVEL.second, }, { source: 'entree_pmr', values: ['True'], targets: ['entree_pmr_informations'], - indent: INDENTS_LEVEL.first, }, // Accueil { source: 'accueil_audiodescription_presence', values: ['True'], targets: ['accueil_audiodescription'], - indent: INDENTS_LEVEL.first, }, { source: 'accueil_equipements_malentendants_presence', values: ['True'], targets: ['accueil_equipements_malentendants'], - indent: INDENTS_LEVEL.first, }, { source: 'accueil_cheminement_plain_pied', @@ -131,7 +112,6 @@ const rules = [ 'accueil_cheminement_rampe', 'accueil_cheminement_ascenseur', ], - indent: INDENTS_LEVEL.first, }, { source: 'accueil_cheminement_ascenseur', @@ -142,20 +122,47 @@ const rules = [ source: 'accueil_ascenseur_etage', values: ['True'], targets: ['accueil_ascenseur_etage_pmr'], - indent: INDENTS_LEVEL.first, }, { source: 'accueil_soignant', values: ['True'], targets: ['accueil_soignant_experience'], - indent: INDENTS_LEVEL.first, + }, + { + source: 'accueil_tribunes_places', + minValue: 1, + targets: ['accueil_tribunes_localisation_places', 'accueil_tribunes_places_avec_accompagnants'], + }, + { + source: 'accueil_vestiaires', + values: ['True'], + targets: ['accueil_vestiaires_largeur_passage'], + }, + { + source: 'accueil_douches_collectives', + values: ['True'], + targets: ['accueil_douches_collectives_adaptees'], + }, + { + source: 'accueil_douches_individuelles', + values: ['True'], + targets: ['accueil_douches_individuelles_adaptees'], + }, + { + source: 'accueil_casiers', + values: ['True'], + targets: ['accueil_casiers_adaptes', 'accueil_casiers_fermeture'], }, // Sanitaires { source: 'sanitaires_presence', values: ['True'], - targets: ['sanitaires_adaptes'], - indent: INDENTS_LEVEL.first, + targets: ['sanitaires_adaptes', 'sanitaires_urinoirs'], + }, + { + source: 'sanitaires_adaptes', + values: ['True'], + targets: ['sanitaires_largeur_porte', 'sanitaires_sens_transfert'], }, // Chambres accessibles { @@ -168,7 +175,6 @@ const rules = [ 'accueil_chambre_sanitaires_barre_appui', 'accueil_chambre_sanitaires_espace_usage', ], - indent: INDENTS_LEVEL.first, }, // Labels // TODO @@ -178,20 +184,17 @@ const rules = [ source: 'user_type', values: ['gestionnaire', 'admin'], targets: ['registre_url'], - indent: INDENTS_LEVEL.first, }, // b. afficher conformité si administration { source: 'user_type', values: ['admin'], targets: ['conformite'], - indent: INDENTS_LEVEL.first, }, { source: 'user_type', values: ['gestionnaire'], targets: ['rpa_exemption'], - indent: INDENTS_LEVEL.first, }, ] diff --git a/templates/contrib/3-transport.html b/templates/contrib/3-transport.html index 8285b68930..1ab42c1422 100644 --- a/templates/contrib/3-transport.html +++ b/templates/contrib/3-transport.html @@ -51,5 +51,8 @@ {% include "fields/boolradio.html" with field=form.stationnement_ext_pmr key="stationnement_ext_pmr" inline=True %} +
+ {% include "fields/boolradio.html" with field=form.stationnement_zone_depose_pmr key="stationnement_zone_depose_pmr" inline=True %} +
{% endblock fields_content %} diff --git a/templates/contrib/4-exterieur.html b/templates/contrib/4-exterieur.html index bbad6a3609..3f7cff95ca 100644 --- a/templates/contrib/4-exterieur.html +++ b/templates/contrib/4-exterieur.html @@ -26,6 +26,9 @@ noindex, nofollow {% endblock robots %} {% block fields_content %} +
+    {{ form.cheminement_ext_signaletique_exterieure.label }}
+    
{% include "fields/boolradio.html" with field=form.cheminement_ext_presence inline=True key="cheminement_ext_presence" %} diff --git a/templates/contrib/6-accueil.html b/templates/contrib/6-accueil.html index 87c006a124..9cc9ce6f88 100644 --- a/templates/contrib/6-accueil.html +++ b/templates/contrib/6-accueil.html @@ -35,6 +35,66 @@ {% include "fields/boolradio.html" with field=form.accueil_signaletique_interieure inline=True key="accueil_signaletique_interieure" %}
{% endif %} + {% if form.accueil_physique %} +
+ {% include "fields/boolradio.html" with field=form.accueil_physique inline=True key="accueil_physique" %} +
+ {% endif %} + {% if form.accueil_aire_de_jeux %} +
+ {% include "fields/boolradio.html" with field=form.accueil_aire_de_jeux inline=True key="accueil_aire_de_jeux" %} +
+ {% endif %} + {% if form.accueil_tribunes %} +
+ {% include "fields/boolradio.html" with field=form.accueil_tribunes inline=True key="accueil_tribunes" %} + {% if form.accueil_tribunes_places %} +
+ {% include "fields/textarea.html" with field=form.accueil_tribunes_places key="accueil_tribunes_places" %} +
+ {% if form.accueil_tribunes_localisation_places %} +
+ {% include "fields/boolradio.html" with field=form.accueil_tribunes_localisation_places inline=True key="accueil_tribunes_localisation_places" %} +
+ {% endif %} + {% if form.accueil_tribunes_places_avec_accompagnants %} +
+ {% include "fields/textarea.html" with field=form.accueil_tribunes_places_avec_accompagnants key="accueil_tribunes_places_avec_accompagnants" %} +
+ {% endif %} + {% endif %} +
+ {% endif %} + {% if form.accueil_vestiaires %} +
+ {% include "fields/boolradio.html" with field=form.accueil_vestiaires inline=True key="accueil_vestiaires" %} + {% if form.accueil_vestiaires_largeur_passage %} +
+ {% include "fields/boolradio.html" with field=form.accueil_vestiaires_largeur_passage inline=True key="accueil_vestiaires_largeur_passage" %} +
+ {% endif %} +
+ {% endif %} + {% if form.accueil_douches_collectives %} +
+ {% include "fields/boolradio.html" with field=form.accueil_douches_collectives inline=True key="accueil_douches_collectives" %} + {% if form.accueil_douches_collectives_adaptees %} +
+ {% include "fields/boolradio.html" with field=form.accueil_douches_collectives_adaptees inline=True key="accueil_douches_collectives_adaptees" %} +
+ {% endif %} +
+ {% endif %} + {% if form.accueil_douches_individuelles %} +
+ {% include "fields/boolradio.html" with field=form.accueil_douches_individuelles inline=True key="accueil_douches_individuelles" %} + {% if form.accueil_douches_individuelles_adaptees %} +
+ {% include "fields/boolradio.html" with field=form.accueil_douches_individuelles_adaptees inline=True key="accueil_douches_individuelles_adaptees" %} +
+ {% endif %} +
+ {% endif %} {% if form.accueil_isoloir_accessibilite %}
{% include "fields/boolradio.html" with field=form.accueil_isoloir_accessibilite inline=True key="accueil_isoloir_accessibilite" %} @@ -99,10 +159,50 @@ {% endif %}
{% include "fields/boolradio.html" with field=form.sanitaires_presence inline=True %} + {% if form.sanitaires_urinoirs %} +
+ {% include "fields/boolradio.html" with field=form.sanitaires_urinoirs inline=True key="sanitaires_urinoirs" %} +
+ {% endif %}
{% include "fields/boolradio.html" with field=form.sanitaires_adaptes inline=True key="sanitaires_adaptes" %}
+ {% if form.sanitaires_largeur_porte %} +
+ {% include "fields/boolradio.html" with field=form.sanitaires_largeur_porte inline=True key="sanitaires_largeur_porte" %} +
+ {% endif %} + {% if form.sanitaires_sens_transfert %} +
+ {% include "fields/boolradio.html" with field=form.sanitaires_sens_transfert inline=True key="sanitaires_sens_transfert" %} +
+ {% endif %}
+ {% if form.accueil_casiers %} +
+ {% include "fields/boolradio.html" with field=form.accueil_casiers inline=True %} + {% if form.accueil_casiers_adaptes %} +
+ {% include "fields/boolradio.html" with field=form.accueil_casiers_adaptes inline=True key="accueil_casiers_adaptes" %} +
+ {% endif %} + {% if form.accueil_casiers_fermeture %} +
+ {% include "fields/checkbox.html" with field=form.accueil_casiers_fermeture inline=True key="accueil_casiers_fermeture" %} +
+ {% endif %} +
+ {% endif %} + {% if form.accueil_prestations_complementaires %} +
+ {% include "fields/checkbox.html" with field=form.accueil_prestations_complementaires inline=True %} +
+ {% endif %} + {% if form.accueil_presence_espaces_specifiques %} +
+ {% include "fields/checkbox.html" with field=form.accueil_presence_espaces_specifiques inline=True %} +
+ {% endif %} {% if form.accueil_ascenseur_etage %}
{% include "fields/boolradio.html" with field=form.accueil_ascenseur_etage key="accueil_ascenseur_etage" %} diff --git a/tests/api/tests.py b/tests/api/tests.py index 634b863f4b..7671a10e21 100644 --- a/tests/api/tests.py +++ b/tests/api/tests.py @@ -125,6 +125,72 @@ def test_list(self, api_client, initial_erp): ) assert "transport" not in erp_json["accessibilite"]["datas"] + def test_list_sports_equipment(self, api_client): + gymnase = ActiviteFactory(nom="Gymnase") + ErpFactory( + nom="Gymnase Jean Moulin", + activite=gymnase, + accessibilite__stationnement_zone_depose_pmr=True, + accessibilite__accueil_physique="non_forme", + accessibilite__accueil_tribunes=True, + accessibilite__accueil_tribunes_places=3, + accessibilite__accueil_tribunes_localisation_places="niveau_aire_de_jeux", + accessibilite__accueil_tribunes_places_avec_accompagnants=1, + accessibilite__accueil_vestiaires=True, + accessibilite__accueil_vestiaires_largeur_passage="superieur_a_110", + accessibilite__accueil_casiers=True, + accessibilite__accueil_casiers_adaptes=False, + accessibilite__accueil_casiers_fermeture=["serrure_avec_cle", "autre"], + accessibilite__accueil_prestations_complementaires=["score_visible"], + accessibilite__accueil_presence_espaces_specifiques=["presence_espace_chiens_guides"], + accessibilite__sanitaires_presence=True, + accessibilite__sanitaires_adaptes=True, + accessibilite__sanitaires_largeur_porte="entre_90_et_110", + accessibilite__sanitaires_sens_transfert="gauche_et_droite", + accessibilite__sanitaires_urinoirs=True, + ) + + response = api_client.get(reverse("erp-list") + "?clean=true") + access = response.json()["results"][0]["accessibilite"] + assert access["transport"]["stationnement_zone_depose_pmr"] is True + assert access["accueil"]["accueil_physique"] == "non_forme" + assert access["accueil"]["accueil_vestiaires_largeur_passage"] == "superieur_a_110" + assert access["accueil"]["sanitaires_largeur_porte"] == "entre_90_et_110" + assert access["accueil"]["sanitaires_sens_transfert"] == "gauche_et_droite" + assert access["accueil"]["accueil_tribunes"] is True + assert access["accueil"]["accueil_tribunes_places"] == 3 + assert access["accueil"]["accueil_tribunes_localisation_places"] == "niveau_aire_de_jeux" + assert access["accueil"]["accueil_tribunes_places_avec_accompagnants"] == 1 + assert access["accueil"]["accueil_vestiaires"] is True + assert access["accueil"]["accueil_casiers"] is True + assert access["accueil"]["accueil_casiers_adaptes"] is False + assert access["accueil"]["accueil_casiers_fermeture"] == ["serrure_avec_cle", "autre"] + assert access["accueil"]["accueil_prestations_complementaires"] == ["score_visible"] + assert access["accueil"]["accueil_presence_espaces_specifiques"] == ["presence_espace_chiens_guides"] + assert access["accueil"]["sanitaires_urinoirs"] is True + + response = api_client.get(reverse("erp-list") + "?readable=true&clean=true") + datas = response.json()["results"][0]["accessibilite"]["datas"] + assert datas["transport"]["stationnement_zone_depose_pmr"] == "Présence d'une zone de dépose PMR" + assert ( + datas["accueil"]["accueil_physique"] + == "Présence de personnel pendant les heures d'ouverture : Personnel non formé" + ) + assert datas["accueil"]["accueil_tribunes"] == "Places spectateurs avec dossier" + assert ( + datas["accueil"]["accueil_tribunes_places"] + == "Places accessibles pour les personnes en fauteuil roulant : 3" + ) + assert ( + datas["accueil"]["accueil_tribunes_localisation_places"] + == "Localisation des places : Places situées au niveau de l'aire de jeux" + ) + assert datas["accueil"]["accueil_tribunes_places_avec_accompagnants"] == ( + "Places accompagnant à côté des places pour les personnes en fauteuil roulant : 1" + ) + assert datas["accueil"]["accueil_casiers_fermeture"] == "Système de fermeture présent : Serrure avec clé, Autre" + assert datas["accueil"]["sanitaires_urinoirs"] == "Urinoirs à différentes hauteurs" + def test_list_geojson(self, api_client, initial_erp): geojson_expected_for_erp = { "type": "FeatureCollection", @@ -326,6 +392,7 @@ def test_detail(self, api_client, initial_erp): "stationnement_pmr": None, "stationnement_ext_presence": None, "stationnement_ext_pmr": None, + "stationnement_zone_depose_pmr": None, "transport_bureau_de_vote_accessibilite": None, }, "cheminement_ext": { @@ -398,6 +465,8 @@ def test_detail(self, api_client, initial_erp): "accueil_equipements_malentendants": [], "sanitaires_presence": True, "sanitaires_adaptes": False, + "sanitaires_largeur_porte": None, + "sanitaires_sens_transfert": None, "accueil_ascenseur_etage": None, "accueil_ascenseur_etage_pmr": None, "accueil_classes_accessibilite": None, @@ -409,6 +478,24 @@ def test_detail(self, api_client, initial_erp): "accueil_salle_consultation_accessible": None, "accueil_consultation_domicile": None, "accueil_prise_en_charge_patients": [], + "accueil_physique": None, + "accueil_aire_de_jeux": None, + "accueil_tribunes": None, + "accueil_tribunes_places": None, + "accueil_tribunes_localisation_places": None, + "accueil_tribunes_places_avec_accompagnants": None, + "accueil_vestiaires": None, + "accueil_vestiaires_largeur_passage": None, + "accueil_douches_collectives": None, + "accueil_douches_collectives_adaptees": None, + "accueil_douches_individuelles": None, + "accueil_douches_individuelles_adaptees": None, + "accueil_casiers": None, + "accueil_casiers_adaptes": None, + "accueil_casiers_fermeture": [], + "accueil_prestations_complementaires": [], + "accueil_presence_espaces_specifiques": [], + "sanitaires_urinoirs": None, }, "registre": {"registre_url": None}, "conformite": {"conformite": None}, @@ -658,6 +745,7 @@ def test_get(self, api_client, initial_erp): "stationnement_pmr": None, "stationnement_ext_presence": None, "stationnement_ext_pmr": None, + "stationnement_zone_depose_pmr": None, }, "cheminement_ext": { "cheminement_ext_presence": None, @@ -729,6 +817,8 @@ def test_get(self, api_client, initial_erp): "accueil_equipements_malentendants": [], "sanitaires_presence": True, "sanitaires_adaptes": False, + "sanitaires_largeur_porte": None, + "sanitaires_sens_transfert": None, "accueil_ascenseur_etage": None, "accueil_ascenseur_etage_pmr": None, "accueil_classes_accessibilite": None, @@ -740,6 +830,24 @@ def test_get(self, api_client, initial_erp): "accueil_salle_consultation_accessible": None, "accueil_consultation_domicile": None, "accueil_prise_en_charge_patients": [], + "accueil_physique": None, + "accueil_aire_de_jeux": None, + "accueil_tribunes": None, + "accueil_tribunes_places": None, + "accueil_tribunes_localisation_places": None, + "accueil_tribunes_places_avec_accompagnants": None, + "accueil_vestiaires": None, + "accueil_vestiaires_largeur_passage": None, + "accueil_douches_collectives": None, + "accueil_douches_collectives_adaptees": None, + "accueil_douches_individuelles": None, + "accueil_douches_individuelles_adaptees": None, + "accueil_casiers": None, + "accueil_casiers_adaptes": None, + "accueil_casiers_fermeture": [], + "accueil_prestations_complementaires": [], + "accueil_presence_espaces_specifiques": [], + "sanitaires_urinoirs": None, }, "registre": {"registre_url": None}, "conformite": {"conformite": None}, diff --git a/tests/erp/imports/test_serializers.py b/tests/erp/imports/test_serializers.py index d948713c04..0266440804 100644 --- a/tests/erp/imports/test_serializers.py +++ b/tests/erp/imports/test_serializers.py @@ -61,6 +61,56 @@ None, id="array", ), + pytest.param( + { + "accessibilite": { + "stationnement_zone_depose_pmr": "oui", + "accueil_physique": "sensibilise_ou_forme", + "accueil_aire_de_jeux": "non", + "accueil_tribunes": 1, + "accueil_tribunes_places": 3, + "accueil_tribunes_localisation_places": "niveau_aire_de_jeux", + "accueil_tribunes_places_avec_accompagnants": 2, + "accueil_vestiaires": "vrai", + "accueil_vestiaires_largeur_passage": "superieur_a_110", + "accueil_douches_collectives": True, + "accueil_douches_collectives_adaptees": True, + "accueil_douches_individuelles": "faux", + "accueil_casiers": "1", + "accueil_casiers_adaptes": "0", + "accueil_casiers_fermeture": ["serrure_avec_cle", "verrou_cadenas"], + "accueil_prestations_complementaires": ["score_visible"], + "accueil_presence_espaces_specifiques": ["presence_espace_repos_sensoriel"], + "sanitaires_presence": True, + "sanitaires_adaptes": True, + "sanitaires_largeur_porte": "entre_90_et_110", + "sanitaires_sens_transfert": "gauche_et_droite", + "sanitaires_urinoirs": True, + } + }, + True, + None, + id="sports_equipment", + ), + pytest.param( + {"accessibilite": {"accueil_casiers_fermeture": ["unknown_lock"]}}, + False, + None, + id="sports_equipment_invalid_array_choice", + ), + pytest.param( + {"accessibilite": {"accueil_physique": "unknown"}}, + False, + None, + id="sports_equipment_invalid_choice", + ), + pytest.param( + # sanitaires_adaptes is not True, so its children must stay empty + {"accessibilite": {"sanitaires_presence": True, "sanitaires_largeur_porte": "entre_80_et_90"}}, + False, + None, + id="sports_equipment_inconsistent_sanitaires", + ), ), ) def test_erp_import_serializer(mocker, erp_values, is_valid, geocoder_result): @@ -179,6 +229,165 @@ def test_erp_update_serializer_inconsistencies(): assert erp.accessibilite.accueil_cheminement_ascenseur_pmr is True, "control, should not be reinit" +@pytest.mark.django_db +def test_erp_import_serializer_sports_equipment(mocker): + ActiviteFactory(nom="Gymnase") + CommuneFactory(nom="Jacou") + mocker.patch( + "erp.provider.geocoder.geocode", + return_value={ + "geom": Point((0, 0), srid=4326), + "numero": "4", + "voie": "Rue de la Paix", + "lieu_dit": None, + "code_postal": "34830", + "commune": "Jacou", + "code_insee": "34120", + "provider": "ban", + }, + ) + + serializer = ErpImportSerializer( + data={ + "voie": "Rue de la Coquille", + "code_postal": 34830, + "commune": "Jacou", + "nom": "Gymnase Jean Moulin", + "activite": "Gymnase", + "accessibilite": { + "stationnement_zone_depose_pmr": "oui", + "accueil_physique": "non_forme", + "accueil_aire_de_jeux": "non", + "accueil_tribunes": "vrai", + "accueil_tribunes_places": 3, + "accueil_tribunes_localisation_places": "reparti_differents_niveaux", + "accueil_tribunes_places_avec_accompagnants": 1, + "accueil_vestiaires": 1, + "accueil_vestiaires_largeur_passage": "entre_90_et_110", + "accueil_douches_collectives": "1", + "accueil_douches_collectives_adaptees": "0", + "accueil_douches_individuelles": "faux", + "accueil_casiers": True, + "accueil_casiers_adaptes": "oui", + "accueil_casiers_fermeture": ["serrure_electronique_a_code", "autre"], + "accueil_prestations_complementaires": ["sonorisation_arbitrage", "score_visible"], + "accueil_presence_espaces_specifiques": ["presence_espace_chiens_guides"], + "sanitaires_presence": "oui", + "sanitaires_adaptes": "oui", + "sanitaires_largeur_porte": "entre_80_et_90", + "sanitaires_sens_transfert": "gauche_et_droite", + "sanitaires_urinoirs": "non", + }, + } + ) + + assert serializer.is_valid(), serializer.errors + erp = serializer.save() + + access = erp.accessibilite + assert access.stationnement_zone_depose_pmr is True + assert access.accueil_physique == "non_forme" + assert access.accueil_aire_de_jeux is False + assert access.accueil_tribunes is True + assert access.accueil_tribunes_places == 3 + assert access.accueil_tribunes_localisation_places == "reparti_differents_niveaux" + assert access.accueil_tribunes_places_avec_accompagnants == 1 + assert access.accueil_vestiaires is True + assert access.accueil_vestiaires_largeur_passage == "entre_90_et_110" + assert access.accueil_douches_collectives is True + assert access.accueil_douches_collectives_adaptees is False + assert access.accueil_douches_individuelles is False + assert access.accueil_casiers is True + assert access.accueil_casiers_adaptes is True + assert access.accueil_casiers_fermeture == ["serrure_electronique_a_code", "autre"] + assert access.accueil_prestations_complementaires == ["sonorisation_arbitrage", "score_visible"] + assert access.accueil_presence_espaces_specifiques == ["presence_espace_chiens_guides"] + assert access.sanitaires_presence is True + assert access.sanitaires_adaptes is True + assert access.sanitaires_largeur_porte == "entre_80_et_90" + assert access.sanitaires_sens_transfert == "gauche_et_droite" + assert access.sanitaires_urinoirs is False + + +@pytest.mark.django_db +def test_erp_update_serializer_sports_equipment_inconsistencies(): + erp = ErpFactory( + accessibilite__accueil_tribunes=True, + accessibilite__accueil_tribunes_places=4, + accessibilite__accueil_tribunes_localisation_places="niveau_aire_de_jeux", + accessibilite__accueil_tribunes_places_avec_accompagnants=2, + accessibilite__accueil_vestiaires=True, + accessibilite__accueil_vestiaires_largeur_passage="superieur_a_110", + accessibilite__accueil_casiers=True, + accessibilite__accueil_casiers_adaptes=True, + accessibilite__accueil_casiers_fermeture=["serrure_avec_cle"], + accessibilite__accueil_douches_collectives=True, # control + accessibilite__accueil_douches_collectives_adaptees=True, # control + accessibilite__sanitaires_presence=True, + accessibilite__sanitaires_adaptes=True, + accessibilite__sanitaires_largeur_porte="entre_90_et_110", + accessibilite__sanitaires_sens_transfert="gauche", + accessibilite__sanitaires_urinoirs=True, + ) + + serializer = ErpImportSerializer( + instance=erp, + data={ + "accessibilite": { + "accueil_tribunes_places": 0, + "accueil_vestiaires": False, + "accueil_casiers": "non", + # sanitaires_presence is repeated as a partial payload is validated on its own + "sanitaires_presence": True, + "sanitaires_adaptes": False, + } + }, + partial=True, + ) + assert serializer.is_valid(), serializer.errors + serializer.save() + + erp.refresh_from_db() + access = erp.accessibilite + assert access.accueil_tribunes_places == 0 + assert access.accueil_tribunes_localisation_places is None, "should have reinit child of attr switched to 0" + assert access.accueil_tribunes_places_avec_accompagnants is None, "should have reinit child of attr switched to 0" + assert access.accueil_vestiaires is False + assert access.accueil_vestiaires_largeur_passage is None, "should have reinit child" + assert access.accueil_casiers is False + assert access.accueil_casiers_adaptes is None, "should have reinit child" + assert access.accueil_casiers_fermeture == [], "should have reinit array child" + assert access.sanitaires_adaptes is False + assert access.sanitaires_largeur_porte is None, "should have reinit child" + assert access.sanitaires_sens_transfert is None, "should have reinit child" + assert access.accueil_douches_collectives is True, "control, should not be reinit" + assert access.accueil_douches_collectives_adaptees is True, "control, should not be reinit" + + +@pytest.mark.django_db +def test_erp_update_serializer_keeps_children_above_min_value(): + erp = ErpFactory( + accessibilite__accueil_tribunes=True, + accessibilite__accueil_tribunes_places=4, + accessibilite__accueil_tribunes_localisation_places="niveau_aire_de_jeux", + accessibilite__accueil_tribunes_places_avec_accompagnants=2, + ) + + serializer = ErpImportSerializer( + instance=erp, + data={"accessibilite": {"accueil_tribunes_places": 8}}, + partial=True, + ) + assert serializer.is_valid(), serializer.errors + serializer.save() + + erp.refresh_from_db() + access = erp.accessibilite + assert access.accueil_tribunes_places == 8 + assert access.accueil_tribunes_localisation_places == "niveau_aire_de_jeux", "children must be kept above min value" + assert access.accueil_tribunes_places_avec_accompagnants == 2, "children must be kept above min value" + + @pytest.mark.django_db def test_erp_duplicate(): boulangerie = ActiviteFactory(nom="Boulangerie") diff --git a/tests/erp/test_forms.py b/tests/erp/test_forms.py index e70345eb65..962cddc547 100644 --- a/tests/erp/test_forms.py +++ b/tests/erp/test_forms.py @@ -1,7 +1,14 @@ import pytest from erp import forms, schema -from tests.factories import CommuneFactory, ErpFactory +from erp.models import ACTIVITY_GROUPS +from tests.factories import ( + AccessibiliteFactory, + ActiviteFactory, + ActivitiesGroupFactory, + CommuneFactory, + ErpFactory, +) @pytest.mark.django_db @@ -47,3 +54,60 @@ def test_ProviderGlobalSearchForm(): assert form.initial["commune_search"] == "Jacou (34 - Hérault)" assert form.initial["code_insee"] == commune.code_insee + + +def _combined_form_for(access, section): + return forms.CombinedAccessibiliteForm( + forms.get_contrib_forms_for_activity(access.erp.activite), + schema.get_section_fields(section), + instance=access, + ) + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "section,field", + ( + (schema.SECTION_ACCUEIL, "accueil_signaletique_interieure"), + (schema.SECTION_CHEMINEMENT_EXT, "cheminement_ext_signaletique_exterieure"), + ), +) +def test_sports_equipment_form_keeps_whitelisted_large_establishments_fields(section, field): + activity = ActiviteFactory(slug="gymnase", nom="Gymnase") + + ActivitiesGroupFactory(activities=[activity], name=ACTIVITY_GROUPS["SPORTS_EQUIPMENT"]) + + access = AccessibiliteFactory(erp=ErpFactory(activite=activity)) + form = _combined_form_for(access, section) + + assert field in form.fields + assert form.fields[field].label == schema.get_label(field) + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "section,field", + ( + (schema.SECTION_ACCUEIL, "accueil_signaletique_interieure"), + (schema.SECTION_CHEMINEMENT_EXT, "cheminement_ext_signaletique_exterieure"), + ), +) +def test_school_form_excludes_non_whitelisted_large_establishments_fields(section, field): + activity = ActiviteFactory(slug="ecole", nom="École") + ActivitiesGroupFactory(activities=[activity], name=ACTIVITY_GROUPS["SCHOOL"]) + access = AccessibiliteFactory(erp=ErpFactory(activite=activity)) + form = _combined_form_for(access, section) + + assert field not in form.fields + + +@pytest.mark.django_db +def test_school_form_keeps_whitelisted_large_establishments_fields(): + activity = ActiviteFactory(slug="ecole", nom="École") + ActivitiesGroupFactory(activities=[activity], name=ACTIVITY_GROUPS["SCHOOL"]) + access = AccessibiliteFactory(erp=ErpFactory(activite=activity)) + form = _combined_form_for(access, schema.SECTION_ACCUEIL) + + for field in forms.ContribAccessibiliteSchoolsForm.large_establishments_fields_to_keep: + assert field in form.fields + assert form.fields[field].label == schema.get_label(field) diff --git a/tests/erp/test_tasks.py b/tests/erp/test_tasks.py index 973c863757..576ab70905 100644 --- a/tests/erp/test_tasks.py +++ b/tests/erp/test_tasks.py @@ -3,6 +3,34 @@ from erp.tasks import compute_access_completion_rate from tests.factories import AccessibiliteFactory, ActiviteFactory, ActivitiesGroupFactory, ErpFactory +# Roots of the "sports_equipment" conditional, always exposed for that activity group +SPORTS_EQUIPMENT_ROOT_FIELDS = ( + "stationnement_zone_depose_pmr", + "accueil_physique", + "accueil_aire_de_jeux", + "accueil_tribunes", + "accueil_tribunes_places", + "accueil_vestiaires", + "accueil_douches_collectives", + "accueil_douches_individuelles", + "accueil_casiers", + "accueil_prestations_complementaires", + "accueil_presence_espaces_specifiques", +) +# Only exposed once their parent is filled in with a value triggering the children +SPORTS_EQUIPMENT_CHILD_FIELDS = ( + "accueil_tribunes_localisation_places", + "accueil_tribunes_places_avec_accompagnants", + "accueil_vestiaires_largeur_passage", + "accueil_douches_collectives_adaptees", + "accueil_douches_individuelles_adaptees", + "accueil_casiers_adaptes", + "accueil_casiers_fermeture", + "sanitaires_urinoirs", + "sanitaires_largeur_porte", + "sanitaires_sens_transfert", +) + @mark.django_db def test_compute_completion_rate(): @@ -134,6 +162,153 @@ def test_compute_completion_rate_healthcare(): assert access.completion_rate == 15 +def _sports_equipment_access(**kwargs): + activity = ActiviteFactory(slug="gymnase", nom="Gymnase") + ActivitiesGroupFactory(activities=[activity], name="Équipements sportifs") + return AccessibiliteFactory(erp=ErpFactory(activite=activity), **kwargs) + + +@mark.django_db +def test_compute_completion_rate_sports_equipment_without_children(): + access = _sports_equipment_access() + + exposed_fields = access.get_exposed_fields() + for field in SPORTS_EQUIPMENT_ROOT_FIELDS: + assert field in exposed_fields, f"{field} should be exposed for sports equipments" + # no parent is filled in yet, so no child is exposed + for field in SPORTS_EQUIPMENT_CHILD_FIELDS: + assert field not in exposed_fields, f"{field} should not be exposed without its parent" + + compute_access_completion_rate(access.pk) + access.refresh_from_db() + assert access.completion_rate == 0 + + # fill in every root, keeping all parents falsy so that no child gets exposed + access.stationnement_zone_depose_pmr = True + access.accueil_physique = "inexistant" + access.accueil_aire_de_jeux = False + access.accueil_tribunes = False + access.accueil_tribunes_places = 0 + access.accueil_vestiaires = False + access.accueil_douches_collectives = False + access.accueil_douches_individuelles = False + access.accueil_casiers = False + access.accueil_prestations_complementaires = ["score_visible"] + access.accueil_presence_espaces_specifiques = ["presence_espace_chiens_guides"] + access.sanitaires_presence = False + access.save() + + compute_access_completion_rate(access.pk) + access.refresh_from_db() + + exposed_fields = access.get_exposed_fields() + assert set(SPORTS_EQUIPMENT_ROOT_FIELDS).issubset(exposed_fields) + assert not set(SPORTS_EQUIPMENT_CHILD_FIELDS) & exposed_fields + assert access.completion_rate == 41 + + +@mark.django_db +def test_compute_completion_rate_sports_equipment_with_children(): + access = _sports_equipment_access( + accueil_tribunes=False, + accueil_vestiaires=False, + accueil_douches_collectives=False, + accueil_douches_individuelles=False, + accueil_casiers=False, + sanitaires_presence=False, + ) + + # each child is exposed only once its own parent displays it + exposed_fields = access.get_exposed_fields() + + for field in SPORTS_EQUIPMENT_CHILD_FIELDS: + assert field not in exposed_fields, f"{field} should not be exposed while its parent is False" + + access.accueil_tribunes = True + access.accueil_vestiaires = True + access.accueil_douches_collectives = True + access.accueil_douches_individuelles = True + access.accueil_casiers = True + access.sanitaires_presence = True + access.save() + + exposed_fields = access.get_exposed_fields() + + # direct children of the truthy roots + assert "accueil_vestiaires_largeur_passage" in exposed_fields + assert "accueil_douches_collectives_adaptees" in exposed_fields + assert "accueil_douches_individuelles_adaptees" in exposed_fields + assert "accueil_casiers_adaptes" in exposed_fields + assert "accueil_casiers_fermeture" in exposed_fields + assert "sanitaires_urinoirs" in exposed_fields + + # children of "sanitaires_adaptes", which is not truthy yet + assert "sanitaires_largeur_porte" not in exposed_fields + assert "sanitaires_sens_transfert" not in exposed_fields + + # nested children of "accueil_tribunes_places", not exposed below its min value + assert "accueil_tribunes_localisation_places" not in exposed_fields + assert "accueil_tribunes_places_avec_accompagnants" not in exposed_fields + + compute_access_completion_rate(access.pk) + access.refresh_from_db() + assert access.completion_rate == 16 + + access.sanitaires_adaptes = True + access.save() + + compute_access_completion_rate(access.pk) + access.refresh_from_db() + exposed_fields = access.get_exposed_fields() + + assert "sanitaires_largeur_porte" in exposed_fields + assert "sanitaires_sens_transfert" in exposed_fields + assert access.completion_rate == 18 + + access.accueil_tribunes_places = 4 + access.save() + + compute_access_completion_rate(access.pk) + access.refresh_from_db() + exposed_fields = access.get_exposed_fields() + + assert "accueil_tribunes_localisation_places" in exposed_fields + assert "accueil_tribunes_places_avec_accompagnants" in exposed_fields + assert access.completion_rate == 20 + + # fill in every remaining sports equipment field + access.stationnement_zone_depose_pmr = True + access.accueil_physique = "sensibilise_ou_forme" + access.accueil_aire_de_jeux = True + access.accueil_prestations_complementaires = ["score_visible"] + access.accueil_presence_espaces_specifiques = ["presence_espace_chiens_guides"] + access.accueil_tribunes_localisation_places = "niveau_aire_de_jeux" + access.accueil_tribunes_places_avec_accompagnants = 2 + access.accueil_vestiaires_largeur_passage = "superieur_a_110" + access.accueil_douches_collectives_adaptees = True + access.accueil_douches_individuelles_adaptees = False + access.accueil_casiers_adaptes = True + access.accueil_casiers_fermeture = ["serrure_avec_cle"] + access.sanitaires_urinoirs = True + access.sanitaires_largeur_porte = "entre_90_et_110" + access.sanitaires_sens_transfert = "gauche_et_droite" + access.save() + + compute_access_completion_rate(access.pk) + + access.refresh_from_db() + exposed_fields = access.get_exposed_fields() + + assert set(SPORTS_EQUIPMENT_ROOT_FIELDS + SPORTS_EQUIPMENT_CHILD_FIELDS).issubset(exposed_fields) + + filled_in = access.get_filled_in_fields() + + for field in SPORTS_EQUIPMENT_ROOT_FIELDS + SPORTS_EQUIPMENT_CHILD_FIELDS: + assert field in filled_in, f"{field} should count as filled in" + + assert access.completion_rate == 57 + + @mark.django_db def test_compute_completion_rate_large_establishments(): activity = ActiviteFactory(slug="centre-commercial", nom="Centre commercial")