diff --git a/back/dora/data_inclusion/mappings.py b/back/dora/data_inclusion/mappings.py index 1d6e943a3..20565b83d 100644 --- a/back/dora/data_inclusion/mappings.py +++ b/back/dora/data_inclusion/mappings.py @@ -78,6 +78,7 @@ def map_search_result(result: dict, supported_service_kinds: list[str]) -> dict: "location_kinds": location_kinds, "kinds": kinds, "fee_condition": service_data["frais"][0] if service_data["frais"] else None, + "funding_labels": [], "modification_date": service_data["date_maj"], "name": service_data["nom"], "short_desc": service_data["presentation_resume"] or "", @@ -257,6 +258,8 @@ def map_service(service_data: dict, is_authenticated: bool) -> dict: "forms": None, "forms_info": None, "full_desc": service_data["presentation_detail"] or "", + "funding_labels": [], + "funding_labels_display": [], "geom": None, "has_already_been_unpublished": None, "is_available": True, diff --git a/back/dora/services/admin.py b/back/dora/services/admin.py index 366b0a23e..397f11e1f 100644 --- a/back/dora/services/admin.py +++ b/back/dora/services/admin.py @@ -10,6 +10,7 @@ CoachOrientationMode, ConcernedPublic, Credential, + FundingLabel, LocationKind, Requirement, SavedSearch, @@ -208,3 +209,4 @@ class SavedSearchAdmin(admin.ModelAdmin): admin.site.register(ServiceKind, EnumAdmin) admin.site.register(ServiceSubCategory, EnumAdmin) admin.site.register(ServiceSource, EnumAdmin) +admin.site.register(FundingLabel, EnumAdmin) diff --git a/back/dora/services/migrations/0111_fundinglabel_service_funding_labels.py b/back/dora/services/migrations/0111_fundinglabel_service_funding_labels.py new file mode 100644 index 000000000..239dcaa53 --- /dev/null +++ b/back/dora/services/migrations/0111_fundinglabel_service_funding_labels.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.16 on 2024-10-31 16:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("services", "0110_remove_service_fee_pass_numerique"), + ] + + operations = [ + migrations.CreateModel( + name="FundingLabel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("value", models.CharField(db_index=True, max_length=255, unique=True)), + ("label", models.CharField(max_length=255)), + ], + options={ + "verbose_name": "Label de financement", + "verbose_name_plural": "Labels de financement", + }, + ), + migrations.AddField( + model_name="service", + name="funding_labels", + field=models.ManyToManyField( + blank=True, + to="services.fundinglabel", + verbose_name="Labels de financement", + ), + ), + ] diff --git a/back/dora/services/migrations/0112_add_funding_labels.py b/back/dora/services/migrations/0112_add_funding_labels.py new file mode 100644 index 000000000..f82a79376 --- /dev/null +++ b/back/dora/services/migrations/0112_add_funding_labels.py @@ -0,0 +1,334 @@ +# Generated by Django 4.2.16 on 2024-11-04 16:43 + +from django.db import migrations + +FUNDED_SERVICE_SLUGS_AVEYRON = [ + "ass-pour-droit-a-l-i-dwdv-microcredit-mobilite-lhjo", + "ass-pour-droit-a-l-i-dwdv-microcredit-mobilite-mlhr", + "ass-pour-droit-a-l-i-dwdv-microcredit-mobilite", + "ass-pour-droit-a-l-i-dwdv-", + "ass-pour-droit-a-l-i-dwdv--kivw", + "ass-pour-droit-a-l-i-dwdv-microcredit-professi-vdlf", + "ass-pour-droit-a-l-i-dwdv-microcredit-professi-dgnl", + "ass-pour-droit-a-l-i-dwdv-microcredit-professi-jkaf", + "ass-pour-droit-a-l-i-dwdv-microcredit-professi", + "association-myriade-information-et-accom", + "association-myriade-apprendre-le-francai", + "comite-rouergat-daid-francais-langue-etra-ivkb", + "comite-rouergat-daid-francais-langue-etra-zmxk", + "comite-rouergat-daid-alphabetisation-lutt-mjwn", + "comite-rouergat-daid-alphabetisation-lutt", + "comite-rouergat-daid-francais-langue-etra", + "comite-rouergat-daid-alphabetisation-lutt-sgcx", + "comite-rouergat-daid-alphabetisation-lutt-sgcx", + "comite-rouergat-daid-francais-langue-etra-yssh", + "comite-rouergat-daid-francais-langue-etra-jsru", + "centre-dinformation--tojo-information-et-accom", + "vacances-familles-v--vacances-pour-tous", + "village-12-ateliers-de-francais", + "village-12-accompagnement-socia", + "village-12-mobilite-des-demande", + "acces-logement-inser-centre-dhebergement--hmvm", +] + +FUNDED_SERVICE_SLUGS_DROME = [ + "agir-innover-mobilis-on-a-des-projets", + "agir-innover-mobilis-et-si-on-relevait-le-pfaj", + "agir-innover-mobilis-et-si-on-relevait-le-kwyq", + "agir-innover-mobilis-et-si-on-relevait-le-andd", + "agir-innover-mobilis-et-si-on-relevait-le-gjsi", + "agir-innover-mobilis-identification-des-m", + "agir-innover-mobilis-identification-des-m-mulq", + "agir-innover-mobilis-identification-des-m-wktt", + "agir-innover-mobilis-identification-des-m-plbm", + "agir-innover-mobilis-identification-des-m-wxsn", + "ancre-et-si-on-decouvrait--ycqw", + "ancre-et-si-on-decouvrait--gcmr", + "ancre-et-si-on-decouvrait--zvmy", + "ancre-et-si-on-decouvrait--otlp", + "ancre-et-si-on-decouvrait--zglv", + "archer-levee-des-freins-jin-seis", + "archer-levee-des-freins-jin-wiia", + "archer-levee-des-freins-jin-ltab", + "archer-levee-des-freins-jin-nogy", + "archer-levee-des-freins-jin-fhmn", + "archer-levee-des-freins-jin-veqv", + "archer-jinterroge-mon-activ", + "archer-levee-des-freins-jin-cnjs", + "bimbamjob-jlgu-vers-un-nouveau-depa", + "bimbamjob-jlgu-vers-un-nouveau-depa-vyea", + "bimbamjob-jlgu-vers-un-nouveau-depa-ulya", + "bimbamjob-jlgu-job-coaching-26", + "cefora-sarl-cefora-vers-un-nouveau-depa", + "cefora-sarl-cefora-vers-un-nouveau-depa-jope", + "plateforme-dinsertio-et-si-on-decouvrait-", + "plateforme-dinsertio-vers-lemploi", + "plateforme-dinsertio-le-permis-de-conduir", + "plateforme-dinsertio-preparation-et-accom-usqu", + "germinal-07-jinterroge-mon-activ", + "germinal-07-jinterroge-mon-activ-ohwe", + "germinal-07-jinterroge-mon-activ-jvaz", + "germinal-07-jinterroge-mon-activ-tasr", + "germinal-07-jinterroge-mon-activ-bmht", + "innovation-developpe-akuu-levee-des-freins-san-lbnw", + "innovation-developpe-akuu-levee-des-freins-san", + "innovation-developpe-akuu-levee-des-freins-san-eoen", + "innovation-developpe-akuu-sante-et-employabili", + "institut-national-de-eyjf-vers-un-nouveau-depa-mcri", + "institut-national-de-eyjf-vers-un-nouveau-depa-vcqc", + "institut-national-de-eyjf-vers-un-nouveau-depa-pnye", + "institut-national-de-eyjf-vers-un-nouveau-depa-dfqc", + "institut-national-de-eyjf-vers-un-nouveau-depa-mbng", + "institut-national-de-eyjf-le-permis-de-conduir", + "institut-national-de-eyjf-le-permis-de-conduir-qbki", + "institut-national-de-eyjf-le-permis-de-conduir-scyr", + "institut-national-de-eyjf-le-permis-de-conduir-fjre", + "institut-national-de-eyjf-le-permis-de-conduir-qrra", + "institut-promotions--identification-des-m-eyqd", + "institut-promotions--identification-des-m-ckis", + "institut-promotions--identification-des-m-wkpq", + "institut-promotions--identification-des-m-ftlr", + "institut-promotions--identification-des-m-qfod", + "institut-promotions--identification-des-m-qsvi", + "institut-promotions--identification-des-m-xjxo", + "institut-promotions--et-si-on-relevait-le-xusy", + "institut-promotions--et-si-on-relevait-le-ejsu", + "institut-promotions--et-si-on-relevait-le-vsve", + "institut-promotions--et-si-on-relevait-le-giyz", + "institut-promotions--et-si-on-relevait-le-tsne", + "institut-promotions--et-si-on-relevait-le-fpzu", + "institut-promotions--et-si-on-relevait-le-jijd", + "institut-promotions--levee-des-freins-san-bwps", + "institut-promotions--levee-des-freins-san-nrpg", + "institut-promotions--levee-des-freins-san-gsfl", + "institut-promotions--levee-des-freins-san-iprt", + "institut-promotions--levee-des-freins-san-ntgz", + "institut-promotions--levee-des-freins-san-diya", + "mobilite-07-26-permis-de-conduire-c-nrfs", + "mobilite-07-26-permis-de-conduire-c-sfja", + "mobilite-07-26-auto-ecole-insertion-pxjt", + "mobilite-07-26-lacces-a-des-vehicul-smkc", + "mobilite-07-26-lacces-a-des-vehicul", + "mobilite-07-26-lacces-a-des-vehicul-tfmg", + "mobilite-07-26-lacces-a-des-vehicul-kbds", + "mobilite-07-26-lacces-a-des-vehicul-qaqa", + "mobilite-07-26-lacces-a-des-vehicul-upjy", + "solerys-vers-un-nouveau-depa-qqif", + "solerys-vers-un-nouveau-depa", + "ass-tremplin-inserti-permis-de-conduire-c-bjzn", + "ass-tremplin-inserti-permis-de-conduire-c-imsn", + "ass-tremplin-inserti-permis-de-conduire-c-uoro", + "ass-tremplin-inserti-permis-de-conduire-a-wllm", + "valence-services-et-si-on-decouvrait-", + "valence-services-rsa-drome-accesentre-cmig", + "valence-services-rsa-drome-accesentre-jbxc", + "valence-services-rsa-drome-accesentre-xdyy", + "valence-services-rsa-drome-accesentre-tall", + "valence-services-rsa-drome-accesentre-rwib", + "valence-services-rsa-drome-accesentre-mwxr", + "valence-services-rsa-drome-accesentre-ikeb", + "valence-services-rsa-drome-accesentre-eiba", + "valence-services-rsa-drome-accesentre-dfso", + "valence-services-rsa-drome-accesentre-dcab", + "valence-services-rsa-arche-agglo-loca-zcpl", + "valence-services-rsa-arche-agglo-loca-rovo", + "valence-services-rsa-arche-agglo-loca-njtl", + "valence-services-rsa-arche-agglo-loca", + "valence-services-rsa-drome-accesentre-adlk", + "valence-services-rsa-drome-accesentre-ojtn", + "valence-services-rsa-porte-de-dromard-upbw", + "valence-services-rsa-porte-de-dromard-kiqh", + "valence-services-rsa-porte-de-dromard-ihlb", + "valence-services-rsa-porte-de-dromard", + "valence-services-rsa-drome-accesentre-xxir", + "valence-services-rsa-drome-accesentre-ombo", + "valence-services-rsa-drome-accesentre-mmql", + "valence-services-rsa-drome-accesentre-lrzt", + "valence-services-rsa-drome-accesentre-htsl", + "valence-services-rsa-drome-accesentre-ebhy", + "valence-services-rsa-drome-accesentre-atpu", + "valence-services-rsa-drome-accesentre", + "aequitaz-collectif-mixte-lutt", + "alter-egal-egal-emploi", + "association-sociale--fdpb-asnit", + "agence-developpement-formation-diplomante", + "centre-communal-dact-alsi-hebergement-durgence", + "centre-communal-dact-alsi-accueil-de-jour-sdf", + "chambre-d-agricultur-rebondir-26-xvat", + "chambre-d-agricultur-rebondir-26-xbzm", + "chambre-d-agricultur-rebondir-26-vmfh", + "chambre-d-agricultur-rebondir-26-rqat", + "chambre-d-agricultur-rebondir-26", + "diaconat-protestant-fpnt-agir-26", + "diaconat-protestant-fpnt-agir-26-iyrg", + "diaconat-protestant-mbld-rsa", + "diaconat-protestant-gkvo-cuisine-portage-de-r", + "dromolib-formation-ecoconduit", + "dromolib-remise-en-selle", + "dromolib-a-bicyclette-un-parc", + "dromolib-mad-velo", + "dromolib-atelier-reparation-v", + "dromolib-plans-de-deplacement", + "dromolib-point-info-mobilite", + "humando-ett-i", + "initiactive-2607-ied-cest-quoi-lentrepren", + "initiactive-2607-ied-marie-noelle-descham", + "initiactive-2607-ied-atelier-strategie-co", + "initiactive-2607-ied-permanence", + "initiactive-2607-ied-creation-dactivite-e", + "initiactive-2607-ied-atelier-financer-son", + "initiactive-2607-ied-atelier-financer-son-uczp", + "initiactive-2607-ied-atelier-gerer-son-ac", + "initiactive-2607-ied-atelier-gerer-son-ac-qacw", + "initiative-portes-de-diagnostic-projet-cr", + "initiative-portes-de-diagnostic-projet-cr-zoca", + "initiative-vallee-de-je-pilote-ma-boite", + "lintergroupe-marcel--formation-de-francai", + "lintergroupe-marcel--ateliers-vers-lemplo", + "la-plateforme-territ-atelier-decouverte-d-vkeo", + "la-plateforme-territ-atelier-savoir-lire-", + "la-plateforme-territ-atelier-mieux-connai", + "la-plateforme-territ-bilan-accompagnement", + "la-plateforme-territ-integracode", + "la-plateforme-territ-100-chances-100-empl", + "la-plateforme-territ-centre-ressources-il-bycd", + "la-plateforme-territ-les-clauses-sociales", + "la-plateforme-territ-bilan-accompagnement-jjaw", + "la-plateforme-territ-ecole-de-la-2e-chanc", + "la-plateforme-territ-comment-consolider-l", + "la-plateforme-territ-atelier-informatique", + "la-plateforme-territ-atelier-remobilisati", + "la-plateforme-territ-atelier-decouverte-p", + "lelien26-animation-sociale", + "lelien26-aide-administrative", + "lelien26-mediation-numerique", + "les-jardins-de-cocag-insertion-profession-ishi", + "les-jardins-de-cocag-vente-legumes-et-fru", + "les-jardins-de-cocag-insertion-profession", + "linstitut-des-vocati-live-linstitut-des-v", + "mission-locale-agglo-iod-intervention-sur", + "mission-locale-agglo-ote", + "mission-locale-agglo-contrat-engagement-j", + "mission-locale-agglo-pacea-parcours-contr", + "mission-locale-agglo-pacea-parcours-contr", + "mission-locale-agglo-atelier-mon-projet", + "mission-locale-agglo-formation-qtig", + "mission-locale-agglo-information-garde-de", + "mission-locale-de-la-lkja-immersion-en-entrepr", + "mission-locale-de-la-lkja-immersion-en-entrepr-ssrw", + "mission-locale-de-la-lkja-immersion-en-entrepr-suvg", + "mission-locale-de-la-lkja-immersion-en-entrepr-ggeo", + "mission-locale-de-la-lkja-contrat-dengagement-", + "mission-locale-de-la-lkja-contrat-dengagement--qepk", + "mission-locale-de-la-lkja-contrat-dengagement--agav", + "mission-locale-de-la-lkja-contrat-dengagement--ozld", + "mission-locale-de-la-lkja-la-mobilite-internat", + "mission-locale-de-la-lkja-la-mobilite-internat-fosy", + "mission-locale-de-la-lkja-la-mobilite-internat-dhwk", + "mission-locale-de-la-lkja-la-mobilite-internat-fjvw", + "mission-locale-de-la-lkja-pacea", + "mission-locale-de-la-lkja-pacea-dbmo", + "mission-locale-de-la-lkja-pacea-taiq", + "mission-locale-de-la-lkja-pacea-tnla", + "mission-locale-de-la-lkja-parrainage-dxvn", + "mission-locale-de-la-lkja-parrainage-qfcp", + "mission-locale-de-la-lkja-parrainage-qelk", + "mission-locale-de-la-lkja-parrainage", + "mission-locale-de-la-lkja-accompagnement-des-b", + "mission-locale-de-la-lkja-accueil-et-accompagn-xmux", + "mission-locale-de-la-lkja-accueil-et-accompagn-pnge", + "mission-locale-de-la-lkja-accueil-et-accompagn", + "mission-locale-de-la-lkja-accueil-et-accompagn-lmxq", + "mission-locale-de-la-lkja-accueil-et-accompagn-rjol", + "mission-locale-de-la-lkja-accueil-et-accompagn-daak", + "mission-locale-de-la-lkja-accueil-et-accompagn-gaws", + "mission-locale-de-la-lkja-bilan-de-sante-ndhz", + "mission-locale-de-la-lkja-bilan-de-sante-czfz", + "mission-locale-de-la-lkja-bilan-de-sante-qoii", + "mission-locale-de-la-lkja-bilan-de-sante", + "mission-locale-de-la-lkja-atelier-de-recherche-whyv", + "mission-locale-de-la-lkja-atelier-de-recherche-zjnw", + "mission-locale-de-la-lkja-atelier-de-recherche-praa", + "mission-locale-de-la-lkja-atelier-de-recherche", + "mission-locale-porte-les-talentueux", + "mission-locale-porte-mediation-numerique", + "mission-locale-porte-accompagnement-des-j-crgm", + "mission-locale-porte-accompagnement-des-j-riyp", + "mission-locale-porte-accompagnement-des-j-sjmq", + "mission-locale-porte-accompagnement-des-b", + "mission-locale-porte-accompagnement-des-a-ddjo", + "mission-locale-porte-atelier-marche-du-tr", + "mission-locale-porte-prendre-un-logement-", + "mission-locale-porte-atelier-cv", + "mission-locale-porte-atelier-lettre-de-mo", + "mission-locale-porte-parrainage", + "mission-locale-porte-atelier-nutrition-je", + "mission-locale-porte-atelier-les-institut", + "mission-locale-porte-pacea", + "mission-locale-porte-atelier-numerique", + "mission-locale-porte-bilans-de-sante", + "mission-locale-porte-psc1", + "nyonsaise-pour-accue-hebergement-durgence", + "nyonsaise-pour-accue-iml-ukraine", + "nyonsaise-pour-accue-alt", + "nyonsaise-pour-accue-accompagnement-vers-", + "nyonsaise-pour-accue-accueil-de-jour", + "organisme-assoc-sout-hebergement-pour-per-tteh", + "partenaires-vallee-d-accueil-et-accompagn-snki", + "partenaires-vallee-d-accueil-et-accompagn-hvog", + "partenaires-vallee-d-accueil-et-accompagn-etvw", + "partenaires-vallee-d-accueil-et-accompagn-dniz", + "partenaires-vallee-d-accueil-et-accompagn-rgzm", + "partenaires-vallee-d-accueil-et-accompagn-brnl", + "partenaires-vallee-d-bilans-de-sante", + "pimms-portes-de-prov-materiel-informatiqu", + "pimms-portes-de-prov-mediation-numerique-lniu", + "solstice-une-image-de-soi-au-", + "diaconat-protestant-wtar-dispositif-incurie-d", + "diaconat-protestant-wtar-domiciliation-val-ac", + "diaconat-protestant-wtar-post-chrs-val-accuei", + "diaconat-protestant-wtar-chrs-val-accueil", + "diaconat-protestant-wtar-pension-de-famille-l", + "diaconat-protestant-wtar-mesures-liees-a-lacc", + "diaconat-protestant-wtar-accueil-de-jour-val-", +] + + +def create_funding_labels(apps, schema_editor): + # Récupération des modèles + FundingLabel = apps.get_model("services", "FundingLabel") + Service = apps.get_model("services", "Service") + + funding_label_cd_aveyron = FundingLabel.objects.create( + label="Conseil départemental de l’Aveyron", value="cd-aveyron" + ) + funding_label_cd_drome = FundingLabel.objects.create( + label="Conseil départemental de la Drôme", value="cd-drome" + ) + + funded_services_aveyron = Service.objects.filter( + slug__in=FUNDED_SERVICE_SLUGS_AVEYRON + ) + funded_services_drome = Service.objects.filter(slug__in=FUNDED_SERVICE_SLUGS_DROME) + + for service in funded_services_aveyron: + service.funding_labels.add(funding_label_cd_aveyron) + + for service in funded_services_drome: + service.funding_labels.add(funding_label_cd_drome) + + +def delete_funding_labels(apps, schema_editor): + FundingLabel = apps.get_model("services", "FundingLabel") + + FundingLabel.objects.filter(value__in=("cd-aveyron", "cd-drome")).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("services", "0111_fundinglabel_service_funding_labels"), + ] + + operations = [ + migrations.RunPython(create_funding_labels, reverse_code=delete_funding_labels), + ] diff --git a/back/dora/services/migrations/0113_savedsearch_funding_labels.py b/back/dora/services/migrations/0113_savedsearch_funding_labels.py new file mode 100644 index 000000000..9c2462fd9 --- /dev/null +++ b/back/dora/services/migrations/0113_savedsearch_funding_labels.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.16 on 2024-11-12 16:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("services", "0112_add_funding_labels"), + ] + + operations = [ + migrations.AddField( + model_name="savedsearch", + name="funding_labels", + field=models.ManyToManyField( + blank=True, + to="services.fundinglabel", + verbose_name="Labels de financement", + ), + ), + ] diff --git a/back/dora/services/models.py b/back/dora/services/models.py index b864c7379..af75df8c8 100644 --- a/back/dora/services/models.py +++ b/back/dora/services/models.py @@ -122,6 +122,12 @@ class Meta: verbose_name_plural = "Types de service" +class FundingLabel(EnumModel): + class Meta: + verbose_name = "Label de financement" + verbose_name_plural = "Labels de financement" + + class BeneficiaryAccessMode(EnumModel): class Meta: verbose_name = "Mode d'orientation bénéficiaire" @@ -246,6 +252,12 @@ class Service(ModerationMixin, models.Model): blank=True, ) + funding_labels = models.ManyToManyField( + FundingLabel, + verbose_name="Labels de financement", + blank=True, + ) + ############ # Conditions @@ -665,6 +677,11 @@ class SavedSearch(models.Model): verbose_name="Lieu de déroulement", blank=True, ) + funding_labels = models.ManyToManyField( + FundingLabel, + verbose_name="Labels de financement", + blank=True, + ) frequency = models.CharField( max_length=10, choices=SavedSearchFrequency.choices, @@ -706,13 +723,17 @@ def get_recent_services(self, cutoff_date): if self.location_kinds.exists(): location_kinds = self.location_kinds.values_list("value", flat=True) + funding_labels = None + if self.funding_labels.exists(): + funding_labels = self.funding_labels.values_list("value", flat=True) + # Récupération des résultats de la recherche from .search import search_services city_code = arrdt_to_main_insee_code(self.city_code) city = get_object_or_404(City, pk=city_code) - results = search_services( + results, metadata = search_services( None, self.city_code, city, @@ -721,6 +742,7 @@ def get_recent_services(self, cutoff_date): kinds, fees, location_kinds, + funding_labels, di_client, ) diff --git a/back/dora/services/search.py b/back/dora/services/search.py index aa4f17628..642289011 100644 --- a/back/dora/services/search.py +++ b/back/dora/services/search.py @@ -19,7 +19,8 @@ from dora.structures.models import Structure from .constants import EXCLUDED_DI_SERVICES_THEMATIQUES -from .serializers import SearchResultSerializer +from .models import FundingLabel +from .serializers import FundingLabelSerializer, SearchResultSerializer from .utils import filter_services_by_city_code MAX_DISTANCE = 50 @@ -238,6 +239,7 @@ def _get_dora_results( kinds: Optional[list[str]] = None, fees: Optional[list[str]] = None, location_kinds: Optional[list[str]] = None, + funding_labels: Optional[list[str]] = None, lat: Optional[float] = None, lon: Optional[float] = None, ): @@ -252,6 +254,7 @@ def _get_dora_results( "location_kinds", "categories", "subcategories", + "funding_labels", "coach_orientation_modes", "beneficiaries_access_modes", ) @@ -274,6 +277,9 @@ def _get_dora_results( if location_kinds: services = services.filter(location_kinds__value__in=location_kinds) + if funding_labels: + services = services.filter(funding_labels__value__in=funding_labels) + with_remote = not location_kinds or "a-distance" in location_kinds with_onsite = not location_kinds or "en-presentiel" in location_kinds @@ -313,7 +319,13 @@ def _get_dora_results( with_onsite, ) - return SearchResultSerializer(results, many=True, context={"request": request}).data + funding_labels_found = FundingLabel.objects.filter(service__in=results).distinct() + + return SearchResultSerializer( + results, many=True, context={"request": request} + ).data, { + "funding_labels": FundingLabelSerializer(funding_labels_found, many=True).data + } def search_services( @@ -325,10 +337,11 @@ def search_services( kinds: Optional[list[str]] = None, fees: Optional[list[str]] = None, location_kinds: Optional[list[str]] = None, + funding_labels: Optional[list[str]] = None, di_client: Optional[data_inclusion.DataInclusionClient] = None, lat: Optional[float] = None, lon: Optional[float] = None, -) -> list[dict]: +) -> (list[dict], dict): """Search services from all available repositories. It always includes results from dora own databases. @@ -339,7 +352,8 @@ def search_services( Note : this is the only point where di_client is "injected" Returns: - A list of search results by SearchResultSerializer. + - A list of search results by SearchResultSerializer. + - A metadata dictionary """ di_results = ( _get_di_results( @@ -357,7 +371,7 @@ def search_services( else [] ) - dora_results = _get_dora_results( + dora_results, metadata = _get_dora_results( request=request, categories=categories, subcategories=subcategories, @@ -366,9 +380,10 @@ def search_services( kinds=kinds, fees=fees, location_kinds=location_kinds, + funding_labels=funding_labels, lat=lat, lon=lon, ) all_results = [*dora_results, *di_results] - return _sort_services(all_results) + return _sort_services(all_results), metadata diff --git a/back/dora/services/serializers.py b/back/dora/services/serializers.py index ee3d2ac78..3382340f4 100644 --- a/back/dora/services/serializers.py +++ b/back/dora/services/serializers.py @@ -22,6 +22,7 @@ CoachOrientationMode, ConcernedPublic, Credential, + FundingLabel, LocationKind, Requirement, SavedSearch, @@ -166,6 +167,15 @@ class ServiceSerializer(serializers.ModelSerializer): subcategories_display = serializers.SlugRelatedField( source="subcategories", slug_field="label", many=True, read_only=True ) + funding_labels = serializers.SlugRelatedField( + slug_field="value", + queryset=FundingLabel.objects.all(), + many=True, + required=False, + ) + funding_labels_display = serializers.SlugRelatedField( + source="funding_labels", slug_field="label", many=True, read_only=True + ) access_conditions = CreatablePrimaryKeyRelatedField( many=True, queryset=AccessCondition.objects.all(), @@ -290,6 +300,8 @@ class Meta: "forms", "forms_info", "full_desc", + "funding_labels", + "funding_labels_display", "geom", "has_already_been_unpublished", "is_available", @@ -668,6 +680,15 @@ def __init__(self, *args, **kwargs): location_kinds_display = serializers.SlugRelatedField( source="location_kinds", slug_field="label", many=True, read_only=True ) + funding_labels = serializers.SlugRelatedField( + slug_field="value", + queryset=FundingLabel.objects.all(), + many=True, + required=False, + ) + funding_labels_display = serializers.SlugRelatedField( + source="funding_labels", slug_field="label", many=True, read_only=True + ) new_services_count = serializers.SerializerMethodField() @@ -689,6 +710,8 @@ class Meta: "kinds_display", "location_kinds", "location_kinds_display", + "funding_labels", + "funding_labels_display", "new_services_count", ] @@ -781,13 +804,17 @@ class Meta: fields = [ "address1", "address2", + "beneficiaries_access_modes", "city", + "coach_orientation_modes", "coordinates", "diffusion_zone_type", "distance", + "fee_condition", + "funding_labels", + "is_orientable", "kinds", "location_kinds", - "fee_condition", "modification_date", "name", "postal_code", @@ -795,11 +822,8 @@ class Meta: "short_desc", "slug", "status", - "structure_info", "structure", - "is_orientable", - "coach_orientation_modes", - "beneficiaries_access_modes", + "structure_info", ] def get_distance(self, obj): @@ -808,3 +832,12 @@ def get_distance(self, obj): def get_coordinates(self, obj): if obj.geom: return (obj.geom.x, obj.geom.y) + + +class FundingLabelSerializer(serializers.ModelSerializer): + class Meta: + model = FundingLabel + fields = [ + "value", + "label", + ] diff --git a/back/dora/services/tests/test_services.py b/back/dora/services/tests/test_services.py index 0e685e1a9..8c8df5ea8 100644 --- a/back/dora/services/tests/test_services.py +++ b/back/dora/services/tests/test_services.py @@ -37,6 +37,7 @@ AccessCondition, BeneficiaryAccessMode, CoachOrientationMode, + FundingLabel, LocationKind, Service, ServiceCategory, @@ -256,6 +257,15 @@ def test_update_fee_condition_error(self): ) self.assertEqual(response.status_code, 400) + def test_cant_update_funding_labels(self): + slug = self.my_service.slug + funding_labels = "cd-drome" + response = self.client.patch( + f"/services/{slug}/", + {"funding_labels": funding_labels}, + ) + self.assertEqual(response.status_code, 400) + def test_can_write_field_true(self): response = self.client.get(f"/services/{self.my_service.slug}/") self.assertEqual(response.status_code, 200) @@ -1067,7 +1077,7 @@ def test_find_services_in_city(self): request = self.factory.get("/search/", {"city": self.city1.code}) response = self.search(request) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["id"] == service_data["id"] @@ -1080,9 +1090,11 @@ def test_dont_find_services_in_other_city(self): response = self.search(request) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 - assert ["city_bounds", "services"] == list(response.data.keys()) + assert ["city_bounds", "funding_labels", "services"] == list( + response.data.keys() + ) def test_filter_by_fee(self): service_data = self.make_di_service( @@ -1099,7 +1111,7 @@ def test_filter_by_fee(self): ) response = self.search(request) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert response.data["services"][0]["id"] == service_data["id"] def test_filter_by_kind(self): @@ -1119,7 +1131,7 @@ def test_filter_by_kind(self): ) response = self.search(request) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["id"] == service_data["id"] @@ -1143,7 +1155,7 @@ def test_filter_by_cat(self): ) response = self.search(request) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 2 assert response.data["services"][0]["id"] in [ service_data_1["id"], @@ -1161,7 +1173,7 @@ def test_simple_search_with_data_inclusion(self): response = self.search(request) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["distance"] == 0 assert response.data["services"][0]["id"] == service_data["id"] @@ -1178,7 +1190,7 @@ def test_simple_search_with_data_inclusion_and_dora(self): d = response.data assert response.status_code == 200 - assert len(d) == 2 + assert len(d) == 3 assert len(d["services"]) == 2 assert service_dora.slug in [d["services"][0]["slug"], d["services"][1]["slug"]] assert service_data["id"] in [ @@ -1202,8 +1214,10 @@ def search_services(self, **kwargs): response = self.search(request, di_client) assert response.status_code == 200 # ajout des "city bounds" pour la carte - assert len(response.data) == 2 - assert ["city_bounds", "services"] == list(response.data.keys()) + assert len(response.data) == 3 + assert ["city_bounds", "funding_labels", "services"] == list( + response.data.keys() + ) service, *_ = response.data["services"] assert service["slug"] == service_dora.slug @@ -1232,7 +1246,7 @@ def test_search_target_sources(self): response = self.search(request) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["id"] == service_data["id"] @@ -1745,6 +1759,10 @@ def setUp(self): baker.make("ServiceSubCategory", value="cat2--autre", label="cat2--autre") baker.make("ServiceCategory", value="cat3", label="cat3") + baker.make("FundingLabel", value="funding-label-1", label="Funding label 1") + baker.make("FundingLabel", value="funding-label-2", label="Funding label 2") + baker.make("FundingLabel", value="funding-label-3", label="Funding label 3") + def test_needs_city_code(self): make_service( status=ServiceStatus.PUBLISHED, @@ -1760,7 +1778,7 @@ def test_can_see_published_services(self): ) response = self.client.get(f"/search/?city={self.city1.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service.slug @@ -1770,7 +1788,7 @@ def test_cant_see_draft_services(self): ) response = self.client.get(f"/search/?city={self.city1.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 def test_cant_see_suggested_services(self): @@ -1780,7 +1798,7 @@ def test_cant_see_suggested_services(self): ) response = self.client.get(f"/search/?city={self.city1.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 def test_can_see_service_with_future_suspension_date(self): @@ -1791,7 +1809,7 @@ def test_can_see_service_with_future_suspension_date(self): ) response = self.client.get(f"/search/?city={self.city1.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service.slug @@ -1803,7 +1821,7 @@ def test_cannot_see_service_with_past_suspension_date(self): ) response = self.client.get(f"/search/?city={self.city1.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 def test_find_services_in_city(self): @@ -1814,7 +1832,7 @@ def test_find_services_in_city(self): ) response = self.client.get(f"/search/?city={self.city1.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service.slug @@ -1826,7 +1844,7 @@ def test_find_services_in_epci(self): ) response = self.client.get(f"/search/?city={self.city1.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service.slug @@ -1838,7 +1856,7 @@ def test_find_services_in_dept(self): ) response = self.client.get(f"/search/?city={self.city1.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service.slug @@ -1850,7 +1868,7 @@ def test_find_services_in_region(self): ) response = self.client.get(f"/search/?city={self.city1.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service.slug @@ -1862,7 +1880,7 @@ def test_dont_find_services_in_other_city(self): ) response = self.client.get(f"/search/?city={self.city2.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 def test_dont_find_services_in_other_epci(self): @@ -1873,7 +1891,7 @@ def test_dont_find_services_in_other_epci(self): ) response = self.client.get(f"/search/?city={self.city2.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 def test_dont_find_services_in_other_department(self): @@ -1884,7 +1902,7 @@ def test_dont_find_services_in_other_department(self): ) response = self.client.get(f"/search/?city={self.city2.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 def test_dont_find_services_in_other_region(self): @@ -1895,7 +1913,7 @@ def test_dont_find_services_in_other_region(self): ) response = self.client.get(f"/search/?city={self.city2.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 def test_filter_by_fee_free(self): @@ -1918,7 +1936,7 @@ def test_filter_by_fee_free(self): ) response = self.client.get(f"/search/?city={self.city1.code}&fees=gratuit") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service1.slug @@ -1942,7 +1960,7 @@ def test_filter_by_fee_payant(self): ) response = self.client.get(f"/search/?city={self.city1.code}&fees=payant") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service2.slug @@ -1968,7 +1986,7 @@ def test_filter_by_fee_gratuit_sous_condition(self): f"/search/?city={self.city1.code}&fees=gratuit-sous-conditions" ) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service3.slug @@ -1992,7 +2010,7 @@ def test_filter_without_fee(self): ) response = self.client.get(f"/search/?city={self.city1.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 3 def test_filter_kinds_one(self): @@ -2011,7 +2029,7 @@ def test_filter_kinds_one(self): f"/search/?city={self.city1.code}&kinds={allowed_kinds[0].value}" ) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service1.slug @@ -2036,7 +2054,7 @@ def test_filter_kinds_several(self): f"/search/?city={self.city1.code}&kinds={allowed_kinds[1].value},{allowed_kinds[2].value}" ) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 2 response_slugs = [r["slug"] for r in response.data["services"]] @@ -2059,7 +2077,95 @@ def test_filter_kinds_nomatch(self): f"/search/?city={self.city1.code}&kinds={allowed_kinds[3].value}" ) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 + assert len(response.data["services"]) == 0 + + def test_filter_without_funding(self): + allowed_funding_labels = FundingLabel.objects.all() + make_service( + status=ServiceStatus.PUBLISHED, + diffusion_zone_type=AdminDivisionType.COUNTRY, + ) + make_service( + status=ServiceStatus.PUBLISHED, + diffusion_zone_type=AdminDivisionType.COUNTRY, + funding_labels=[allowed_funding_labels[0]], + ) + make_service( + status=ServiceStatus.PUBLISHED, + diffusion_zone_type=AdminDivisionType.COUNTRY, + funding_labels=[allowed_funding_labels[1], allowed_funding_labels[2]], + ) + response = self.client.get(f"/search/?city={self.city1.code}") + assert response.status_code == 200 + assert len(response.data) == 3 + assert len(response.data["services"]) == 3 + + def test_funding_one(self): + allowed_funding_labels = FundingLabel.objects.all() + service1 = make_service( + status=ServiceStatus.PUBLISHED, + diffusion_zone_type=AdminDivisionType.COUNTRY, + funding_labels=[allowed_funding_labels[0], allowed_funding_labels[1]], + ) + make_service( + status=ServiceStatus.PUBLISHED, + diffusion_zone_type=AdminDivisionType.COUNTRY, + funding_labels=[allowed_funding_labels[2]], + ) + response = self.client.get( + f"/search/?city={self.city1.code}&funding={allowed_funding_labels[0].value}" + ) + assert response.status_code == 200 + assert len(response.data) == 3 + assert len(response.data["services"]) == 1 + assert response.data["services"][0]["slug"] == service1.slug + + def test_funding_several(self): + allowed_funding_labels = FundingLabel.objects.all() + service1 = make_service( + status=ServiceStatus.PUBLISHED, + diffusion_zone_type=AdminDivisionType.COUNTRY, + funding_labels=[allowed_funding_labels[0], allowed_funding_labels[1]], + ) + service2 = make_service( + status=ServiceStatus.PUBLISHED, + diffusion_zone_type=AdminDivisionType.COUNTRY, + funding_labels=[allowed_funding_labels[1], allowed_funding_labels[2]], + ) + make_service( + status=ServiceStatus.PUBLISHED, + diffusion_zone_type=AdminDivisionType.COUNTRY, + funding_labels=[allowed_funding_labels[3]], + ) + response = self.client.get( + f"/search/?city={self.city1.code}&funding={allowed_funding_labels[1].value},{allowed_funding_labels[2].value}" + ) + assert response.status_code == 200 + assert len(response.data) == 3 + assert len(response.data["services"]) == 2 + + response_slugs = [r["slug"] for r in response.data["services"]] + assert service1.slug in response_slugs + assert service2.slug in response_slugs + + def test_funding_nomatch(self): + allowed_funding_labels = FundingLabel.objects.all() + make_service( + status=ServiceStatus.PUBLISHED, + diffusion_zone_type=AdminDivisionType.COUNTRY, + funding_labels=[allowed_funding_labels[0], allowed_funding_labels[1]], + ) + make_service( + status=ServiceStatus.PUBLISHED, + diffusion_zone_type=AdminDivisionType.COUNTRY, + funding_labels=[allowed_funding_labels[1], allowed_funding_labels[2]], + ) + response = self.client.get( + f"/search/?city={self.city1.code}&funding={allowed_funding_labels[3].value}" + ) + assert response.status_code == 200 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 def test_find_service_with_requested_cat(self): @@ -2070,7 +2176,7 @@ def test_find_service_with_requested_cat(self): ) response = self.client.get(f"/search/?city={self.city1.code}&cats=cat1") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service.slug @@ -2087,7 +2193,7 @@ def test_find_service_with_requested_cats(self): ) response = self.client.get(f"/search/?city={self.city1.code}&cats=cat1,cat2") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 2 response_slugs = sorted([s["slug"] for s in response.data["services"]]) @@ -2111,7 +2217,7 @@ def test_find_service_with_requested_cats_exclude_one(self): ) response = self.client.get(f"/search/?city={self.city1.code}&cats=cat1,cat2") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 2 response_slugs = sorted([s["slug"] for s in response.data["services"]]) @@ -2126,7 +2232,7 @@ def test_dont_find_service_without_requested_cat(self): response = self.client.get(f"/search/?city={self.city1.code}&cats=cat2") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 def test_find_service_with_requested_subcat(self): @@ -2137,7 +2243,7 @@ def test_find_service_with_requested_subcat(self): ) response = self.client.get(f"/search/?city={self.city1.code}&subs=cat1--sub1") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service.slug @@ -2156,7 +2262,7 @@ def test_find_service_with_requested_subcats(self): f"/search/?city={self.city1.code}&subs=cat1--sub1,cat1--sub2" ) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 2 response_slugs = sorted([s["slug"] for s in response.data["services"]]) @@ -2188,7 +2294,7 @@ def test_find_service_with_requested_subcats_different_cats(self): ) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 3 response_slugs = sorted([s["slug"] for s in response.data["services"]]) @@ -2216,7 +2322,7 @@ def test_find_service_with_requested_subcats_exclude_one(self): f"/search/?city={self.city1.code}&subs=cat1--sub1,cat1--sub2" ) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 2 response_slugs = sorted([s["slug"] for s in response.data["services"]]) @@ -2231,7 +2337,7 @@ def test_dont_find_service_without_requested_subcat(self): response = self.client.get(f"/search/?city={self.city1.code}&subs=cat1--sub2") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 def test_find_service_with_no_subcat_when_looking_for_the__other__subcat(self): @@ -2244,7 +2350,7 @@ def test_find_service_with_no_subcat_when_looking_for_the__other__subcat(self): ) response = self.client.get(f"/search/?city={self.city1.code}&subs=cat1--autre") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service.slug @@ -2261,7 +2367,7 @@ def test_find_service_with_no_subcat_when_looking_for_the__other__subcat_2( ) response = self.client.get(f"/search/?city={self.city1.code}&subs=cat1--autre") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 assert response.data["services"][0]["slug"] == service.slug @@ -2273,7 +2379,7 @@ def test_dont_find_service_with_no_subcat_when_looking_for_any_subcat(self): ) response = self.client.get(f"/search/?city={self.city1.code}&subs=cat1--sub1") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 def test_find_cats_and_subcats_are_independant(self): @@ -2293,7 +2399,7 @@ def test_find_cats_and_subcats_are_independant(self): f"/search/?city={self.city1.code}&cats=cat1&subs=cat2--sub1" ) assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 2 @@ -2438,7 +2544,7 @@ def test_distance_no_more_than_100km(self): service2.location_kinds.set([LocationKind.objects.get(value="en-presentiel")]) response = self.client.get("/search/?city=31555") - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 def test_displayed_if_remote_and_onsite_more_than_100km(self): @@ -2457,7 +2563,7 @@ def test_displayed_if_remote_and_onsite_more_than_100km(self): ) response = self.client.get("/search/?city=31555") - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 def test_displayed_only_once_if_remote_and_onsite_less_than_100km(self): @@ -2476,7 +2582,7 @@ def test_displayed_only_once_if_remote_and_onsite_less_than_100km(self): ) response = self.client.get("/search/?city=31555") - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 1 def test_intercalate_remote(self): @@ -2779,7 +2885,7 @@ def test_archives_dont_appear_in_search_results_anon(self): response = self.client.get(f"/search/?city={city.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 def test_archives_dont_appear_in_search_results_auth(self): @@ -2792,7 +2898,7 @@ def test_archives_dont_appear_in_search_results_auth(self): response = self.client.get(f"/search/?city={city.code}") assert response.status_code == 200 - assert len(response.data) == 2 + assert len(response.data) == 3 assert len(response.data["services"]) == 0 diff --git a/back/dora/services/tests/test_services_saved_searchs.py b/back/dora/services/tests/test_services_saved_searchs.py index 4dfdff19f..d1f303f5c 100644 --- a/back/dora/services/tests/test_services_saved_searchs.py +++ b/back/dora/services/tests/test_services_saved_searchs.py @@ -26,6 +26,7 @@ "city_label": "Poil (58)", "kinds": ["aide-financiere", "aide-materielle"], "fees": ["gratuit-sous-conditions", "payant"], + "funding_labels": ["funding-label-1", "funding-label-2"], } @@ -55,6 +56,16 @@ def test_create_search(self): value=SAVE_SEARCH_ARGS.get("subcategories")[1], label="cat1--sub2", ) + baker.make( + "FundingLabel", + value=SAVE_SEARCH_ARGS.get("funding_labels")[0], + label=SAVE_SEARCH_ARGS.get("funding_labels")[0], + ) + baker.make( + "FundingLabel", + value=SAVE_SEARCH_ARGS.get("funding_labels")[1], + label=SAVE_SEARCH_ARGS.get("funding_labels")[1], + ) self.assertEqual(SavedSearch.objects.all().count(), 0) @@ -64,6 +75,16 @@ def test_create_search(self): self.assertEqual(response.status_code, 201) self.assertEqual(SavedSearch.objects.all().count(), 1) + saved_search = SavedSearch.objects.first() + self.assertEqual( + sorted(list(saved_search.subcategories.values_list("value", flat=True))), + sorted(SAVE_SEARCH_ARGS.get("subcategories")), + ) + self.assertEqual( + sorted(list(saved_search.funding_labels.values_list("value", flat=True))), + sorted(SAVE_SEARCH_ARGS.get("funding_labels")), + ) + def test_delete_search(self): user = baker.make("users.User", is_valid=True) saved_search = baker.make( diff --git a/back/dora/services/views.py b/back/dora/services/views.py index c1057bab3..40efcbee1 100644 --- a/back/dora/services/views.py +++ b/back/dora/services/views.py @@ -773,6 +773,7 @@ def search(request): kinds = request.GET.get("kinds") fees = request.GET.get("fees") locs = request.GET.get("locs") + funding = request.GET.get("funding") lat = request.GET.get("lat") lon = request.GET.get("lon") @@ -781,6 +782,7 @@ def search(request): kinds_list = kinds.split(",") if kinds is not None else None fees_list = fees.split(",") if fees is not None else None locs_list = locs.split(",") if locs is not None else None + funding_labels_list = funding.split(",") if funding is not None else None lat = float(lat) if lat else None lon = float(lon) if lon else None from .search import search_services @@ -790,7 +792,7 @@ def search(request): di_client = data_inclusion.di_client_factory() - sorted_services = search_services( + sorted_services, metadata = search_services( request=request, di_client=di_client, city_code=city_code, @@ -800,11 +802,18 @@ def search(request): kinds=kinds_list, fees=fees_list, location_kinds=locs_list, + funding_labels=funding_labels_list, lat=lat, lon=lon, ) - return Response({"city_bounds": city.geom.extent, "services": sorted_services}) + return Response( + { + "city_bounds": city.geom.extent, + "funding_labels": metadata["funding_labels"], + "services": sorted_services, + } + ) def share_service(request, service, is_di): diff --git a/front/src/lib/components/specialized/service-search.svelte b/front/src/lib/components/specialized/service-search.svelte index bc154b65c..727028224 100644 --- a/front/src/lib/components/specialized/service-search.svelte +++ b/front/src/lib/components/specialized/service-search.svelte @@ -13,6 +13,7 @@ import type { Choice, FeeCondition, + FundingLabel, LocationKind, ServiceKind, ServicesOptions, @@ -42,6 +43,7 @@ export let kindIds: ServiceKind[] = []; export let feeConditions: FeeCondition[] = []; export let locationKinds: LocationKind[] = []; + export let fundingLabels: Array = []; export let initialSearch = false; let innerWidth; @@ -59,6 +61,7 @@ kindIds, feeConditions, locationKinds, + fundingLabels, lon, lat, }); diff --git a/front/src/lib/components/specialized/services/display/service-key-informations.svelte b/front/src/lib/components/specialized/services/display/service-key-informations.svelte index 23ee9ce6d..5f9056ea3 100644 --- a/front/src/lib/components/specialized/services/display/service-key-informations.svelte +++ b/front/src/lib/components/specialized/services/display/service-key-informations.svelte @@ -1,5 +1,4 @@ -{#if search.subcategories.length || search.kinds.length || search.fees.length} +{#if search.subcategories.length || search.kinds.length || search.fees.length || search.fundingLabels.length}

{#if search.subcategories.length} - Besoins sélectionnés : {search.subcategoriesDisplay.join(", ")}. + Besoins sélectionnés : {search.subcategoriesDisplay.join(", ")}. {:else} Aucun besoin sélectionné. {/if} {#if search.kinds.length} - Type de services : {search.kindsDisplay.join(", ")}. + Type de services : {search.kindsDisplay.join(", ")}. {/if} {#if search.fees.length} - Frais à charge : {search.feesDisplay.join(", ")}. + Frais à charge : {search.feesDisplay.join(", ")}. + {/if} + + {#if search.fundingLabels.length} + Financé par : {search.fundingLabelsDisplay.join(", ")}. {/if}

{/if} diff --git a/front/src/routes/recherche/+page.svelte b/front/src/routes/recherche/+page.svelte index 76707f7d0..f6fb5e3ec 100644 --- a/front/src/routes/recherche/+page.svelte +++ b/front/src/routes/recherche/+page.svelte @@ -6,7 +6,6 @@ import Breadcrumb from "$lib/components/display/breadcrumb.svelte"; import CenteredGrid from "$lib/components/display/centered-grid.svelte"; import SearchForm from "$lib/components/specialized/service-search.svelte"; - import { FUNDED_SERVICES } from "$lib/consts"; import type { ServiceSearchResult } from "$lib/types"; import { userInfo } from "$lib/utils/auth"; import { isInDeploymentDepartments } from "$lib/utils/misc"; @@ -15,10 +14,7 @@ import DoraDeploymentNotice from "./dora-deployment-notice.svelte"; import OnlyNationalResultsNotice from "./only-national-results-notice.svelte"; import ServiceSuggestionNotice from "./service-suggestion-notice.svelte"; - import ResultFilters, { - type Filters, - type FundedByDepartment, - } from "./result-filters.svelte"; + import ResultFilters, { type Filters } from "./result-filters.svelte"; import MapViewButton from "./map-view-button.svelte"; import ResultCount from "./result-count.svelte"; import SearchResults from "./search-results.svelte"; @@ -28,7 +24,7 @@ const FILTER_KEY_TO_QUERY_PARAM = { kinds: "kinds", - fundedBy: "fundedBy", + fundingLabels: "funding", feeConditions: "fees", locationKinds: "locs", }; @@ -55,7 +51,12 @@ }); function resetFilters() { - filters = { kinds: [], fundedBy: [], feeConditions: [], locationKinds: [] }; + filters = { + kinds: [], + fundingLabels: [], + feeConditions: [], + locationKinds: [], + }; } // Réinitialise les filtres quand la recherche est actualisée. @@ -76,10 +77,10 @@ filters.kinds.length === 0 || (service.kinds && filters.kinds.some((value) => service.kinds!.includes(value))); - const fundedByMatch = - filters.fundedBy.length === 0 || - filters.fundedBy.some((department) => - FUNDED_SERVICES[department].slugs.includes(service.slug) + const fundingLabelsMatch = + filters.fundingLabels.length === 0 || + filters.fundingLabels.some((value) => + service.fundingLabels.includes(value) ); const feeConditionMatch = filters.feeConditions.length === 0 || @@ -98,7 +99,7 @@ ); return ( kindsMatch && - fundedByMatch && + fundingLabelsMatch && feeConditionMatch && locationKindsMatch && onSiteAndNearby @@ -134,20 +135,6 @@ data.cityCode && !isInDeploymentDepartments(data.cityCode, data.servicesOptions); - $: fundedByDepartment = - data.cityCode && - (Object.keys(FUNDED_SERVICES).find((department) => - data.cityCode?.startsWith(department) - ) as FundedByDepartment | undefined); - $: fundedByOptions = fundedByDepartment - ? [ - { - value: fundedByDepartment, - label: FUNDED_SERVICES[fundedByDepartment].organism, - }, - ] - : []; - $: showMesAidesDialog = !$userInfo && data.categoryIds.includes("mobilite"); @@ -171,6 +158,7 @@ kindIds={data.kindIds} feeConditions={data.feeConditions} locationKinds={data.locationKinds} + fundingLabels={data.fundingLabels} categoryId={data.categoryIds[0]} subCategoryIds={[...data.subCategoryIds]} showDeploymentWarning={false} @@ -184,10 +172,15 @@ diff --git a/front/src/routes/recherche/+page.ts b/front/src/routes/recherche/+page.ts index 50a3a96eb..55ba8337e 100644 --- a/front/src/routes/recherche/+page.ts +++ b/front/src/routes/recherche/+page.ts @@ -18,10 +18,12 @@ async function getResults({ kindIds, feeConditions, locationKinds, + fundingLabels, lat, lon, }: SearchQuery): Promise<{ cityBounds: [number, number, number, number]; + fundingLabels: Array<{ value: string; label: string }>; services: ServiceSearchResult[]; }> { const querystring = getQueryString({ @@ -33,6 +35,7 @@ async function getResults({ kindIds, feeConditions, locationKinds, + fundingLabels, lat, lon, }); @@ -68,10 +71,17 @@ export const load: PageLoad = async ({ url, parent }) => { const kindIds = query.get("kinds") ? query.get("kinds").split(",") : []; const feeConditions = query.get("fees") ? query.get("fees").split(",") : []; const locationKinds = query.get("locs") ? query.get("locs").split(",") : []; + const fundingLabels = query.get("funding") + ? query.get("funding").split(",") + : []; const lon = query.get("lon"); const lat = query.get("lat"); - const { cityBounds, services } = await getResults({ + const { + cityBounds, + fundingLabels: availableFundingLabels, + services, + } = await getResults({ // La priorité est donnée aux sous-catégories categoryIds: subCategoryIds.length ? [] : categoryIds, subCategoryIds, @@ -82,6 +92,7 @@ export const load: PageLoad = async ({ url, parent }) => { kindIds: [], feeConditions: [], locationKinds: [], + fundingLabels: [], lon, lat, }); @@ -96,6 +107,7 @@ export const load: PageLoad = async ({ url, parent }) => { kindIds, feeConditions, locationKinds, + fundingLabels, services ); @@ -122,6 +134,8 @@ export const load: PageLoad = async ({ url, parent }) => { kindIds, feeConditions, locationKinds, + fundingLabels, + availableFundingLabels, services, servicesOptions: await getServicesOptions(), searchId, diff --git a/front/src/routes/recherche/map-view-button.svelte b/front/src/routes/recherche/map-view-button.svelte index a25e74431..c1c4ce22c 100644 --- a/front/src/routes/recherche/map-view-button.svelte +++ b/front/src/routes/recherche/map-view-button.svelte @@ -1,19 +1,19 @@ @@ -36,13 +31,13 @@ choices={servicesOptions.kinds} bind:group={filters.kinds} /> - {#key fundedByOptions} - {#if fundedByOptions.length > 0} + {#key availableFundingLabels} + {#if availableFundingLabels.length > 0} {/if} {/key} diff --git a/front/src/routes/recherche/search-results.svelte b/front/src/routes/recherche/search-results.svelte index 525a2e800..4a664719b 100644 --- a/front/src/routes/recherche/search-results.svelte +++ b/front/src/routes/recherche/search-results.svelte @@ -43,6 +43,7 @@ kindIds: filters.kinds.sort(), feeConditions: filters.feeConditions.sort(), locationKinds: filters.locationKinds.sort(), + fundingLabels: filters.fundingLabels.sort(), }); const userSavedSearches = $userInfo?.savedSearches || []; @@ -81,6 +82,7 @@ kinds: filters.kinds.sort(), fees: filters.feeConditions.sort(), locationKinds: filters.locationKinds.sort(), + fundingLabels: filters.fundingLabels.sort(), }); await refreshUserInfo(); creatingAlert = false;