From 105fad23cfe50eb88f08238eed0a7fed5d2ca625 Mon Sep 17 00:00:00 2001 From: Chatewgne Date: Thu, 23 Sep 2021 14:22:05 +0200 Subject: [PATCH 01/10] APIv2 : Add filter by ratings on outdoor courses and sites --- docs/changelog.rst | 2 + geotrek/api/locale/fr/LC_MESSAGES/django.po | 5 +- geotrek/api/tests/test_v2.py | 84 +++++++++++++++++++++ geotrek/api/v2/filters.py | 16 ++++ geotrek/api/v2/views/outdoor.py | 6 +- 5 files changed, 110 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4d3c5aaeb5..7f2a363590 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,8 @@ CHANGELOG **New features** +- APIv2 : Add filter by ratings on outdoor courses and sites + 2.65.0 (2021-09-21) ---------------------- diff --git a/geotrek/api/locale/fr/LC_MESSAGES/django.po b/geotrek/api/locale/fr/LC_MESSAGES/django.po index 4f6f559cc4..ebb0b201e2 100644 --- a/geotrek/api/locale/fr/LC_MESSAGES/django.po +++ b/geotrek/api/locale/fr/LC_MESSAGES/django.po @@ -400,4 +400,7 @@ msgid "Only return sites that are at the top of the hierarchy and have no parent msgstr "Ne retourner que les sites qui sont au sommet de la hiérarchie des sites et n'ont pas de parent. Activable par n'importe quelle chaîne de charactères." msgid "Root sites only" -msgstr "Sites racine seulement" \ No newline at end of file +msgstr "Sites racine seulement" + +msgid "Filter by one or more ratings id, comma-separated."" +msgstr "Filtrer par un ou plusieurs id de cotation, séparés par des virgules." \ No newline at end of file diff --git a/geotrek/api/tests/test_v2.py b/geotrek/api/tests/test_v2.py index e939439b28..dbf83d37af 100644 --- a/geotrek/api/tests/test_v2.py +++ b/geotrek/api/tests/test_v2.py @@ -3150,3 +3150,87 @@ def test_course_type_list_returns_published_in_language(self): self.assertNotIn(self.type_with_no_published_course.pk, all_ids) self.assertNotIn(self.type_with_published_and_not_deleted_course.pk, all_ids) self.assertIn(self.type_with_published_and_not_deleted_course_with_lang.pk, all_ids) + + +class SiteFilterByRatingsTestCase(BaseApiTest): + """ Test filtering on ratings for outdoor course + """ + + @classmethod + def setUpTestData(cls): + cls.site1 = outdoor_factory.SiteFactory() + cls.site2 = outdoor_factory.SiteFactory() + cls.rating_scale = outdoor_factory.RatingScaleFactory(practice=cls.site1.practice) + cls.rating1 = outdoor_factory.RatingFactory(scale=cls.rating_scale) + cls.rating2 = outdoor_factory.RatingFactory(scale=cls.rating_scale) + cls.site1.ratings.set([cls.rating1]) + cls.site2.ratings.set([cls.rating2]) + cls.course1 = outdoor_factory.CourseFactory() + cls.course2 = outdoor_factory.CourseFactory() + cls.course1.ratings.set([cls.rating1]) + cls.course2.ratings.set([cls.rating2]) + + def test_site_list_ratings_filter(self): + """ Assert API returns only sites with right ratings + """ + response = self.get_site_list({'ratings': self.rating1.pk}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['count'], 1) + returned_sites = response.json()['results'] + all_ids = [] + for site in returned_sites: + all_ids.append(site['id']) + self.assertIn(self.site1.pk, all_ids) + self.assertNotIn(self.site2.pk, all_ids) + + def test_site_list_ratings_filter2(self): + """ Assert API returns only sites with right ratings + """ + response = self.get_site_list({'ratings': self.rating2.pk}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['count'], 1) + returned_sites = response.json()['results'] + all_ids = [] + for site in returned_sites: + all_ids.append(site['id']) + self.assertIn(self.site2.pk, all_ids) + self.assertNotIn(self.site1.pk, all_ids) + + def test_site_list_ratings_filter3(self): + """ Assert API returns only sites with right ratings + """ + response = self.get_site_list({'ratings': f"{self.rating1.pk},{self.rating2.pk}"}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['count'], 2) + returned_sites = response.json()['results'] + all_ids = [] + for site in returned_sites: + all_ids.append(site['id']) + self.assertIn(self.site1.pk, all_ids) + self.assertIn(self.site2.pk, all_ids) + + def test_course_list_ratings_filter(self): + """ Assert API returns only courses with right ratings + """ + response = self.get_course_list({'ratings': f"{self.rating1.pk},{self.rating2.pk}"}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['count'], 2) + returned_sites = response.json()['results'] + all_ids = [] + for site in returned_sites: + all_ids.append(site['id']) + self.assertIn(self.course1.pk, all_ids) + self.assertIn(self.course2.pk, all_ids) + + def test_course_list_ratings_filter2(self): + """ Assert API returns only courses with right ratings + """ + response = self.get_course_list({'ratings': self.rating2.pk}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['count'], 1) + returned_sites = response.json()['results'] + all_ids = [] + for site in returned_sites: + all_ids.append(site['id']) + self.assertNotIn(self.course1.pk, all_ids) + self.assertIn(self.course2.pk, all_ids) diff --git a/geotrek/api/v2/filters.py b/geotrek/api/v2/filters.py index ae783e8fb6..e7ce509052 100644 --- a/geotrek/api/v2/filters.py +++ b/geotrek/api/v2/filters.py @@ -717,6 +717,22 @@ def get_schema_fields(self, view): ) +class OutdoorRatingsFilter(BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + ratings = request.GET.get('ratings') + if ratings: + queryset = queryset.filter(ratings__in=ratings.split(',')) + return queryset + + def get_schema_fields(self, view): + return Field( + name='ratings', required=False, location='query', schema=coreschema.String( + title=_("Query string"), + description=_('Filter by one or more ratings id, comma-separated.') + ) + ) + + class GeotrekSiteFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): root_sites_only = request.GET.get('root_sites_only') diff --git a/geotrek/api/v2/views/outdoor.py b/geotrek/api/v2/views/outdoor.py index 86821280e3..53caadd064 100644 --- a/geotrek/api/v2/views/outdoor.py +++ b/geotrek/api/v2/views/outdoor.py @@ -12,7 +12,8 @@ class SiteViewSet(api_viewsets.GeotrekGeometricViewset): filter_backends = api_viewsets.GeotrekGeometricViewset.filter_backends + ( api_filters.GeotrekSiteFilter, api_filters.NearbyContentFilter, - api_filters.UpdateOrCreateDateFilter + api_filters.UpdateOrCreateDateFilter, + api_filters.OutdoorRatingsFilter ) serializer_class = api_serializers.SiteSerializer @@ -65,7 +66,8 @@ class CourseViewSet(api_viewsets.GeotrekGeometricViewset): filter_backends = api_viewsets.GeotrekGeometricViewset.filter_backends + ( api_filters.GeotrekCourseFilter, api_filters.NearbyContentFilter, - api_filters.UpdateOrCreateDateFilter + api_filters.UpdateOrCreateDateFilter, + api_filters.OutdoorRatingsFilter ) serializer_class = api_serializers.CourseSerializer From ff91c016e05bbff9ef937179d59cd1f9639937d8 Mon Sep 17 00:00:00 2001 From: Chatewgne Date: Thu, 23 Sep 2021 14:28:44 +0200 Subject: [PATCH 02/10] Remove typo in translations --- geotrek/api/locale/fr/LC_MESSAGES/django.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geotrek/api/locale/fr/LC_MESSAGES/django.po b/geotrek/api/locale/fr/LC_MESSAGES/django.po index ebb0b201e2..1041b1c808 100644 --- a/geotrek/api/locale/fr/LC_MESSAGES/django.po +++ b/geotrek/api/locale/fr/LC_MESSAGES/django.po @@ -402,5 +402,5 @@ msgstr "Ne retourner que les sites qui sont au sommet de la hiérarchie des site msgid "Root sites only" msgstr "Sites racine seulement" -msgid "Filter by one or more ratings id, comma-separated."" -msgstr "Filtrer par un ou plusieurs id de cotation, séparés par des virgules." \ No newline at end of file +msgid "Filter by one or more ratings id, comma-separated." +msgstr "Filtrer par un ou plusieurs id de cotation, séparés par des virgules." From 880148ef817af6f4d4c0ba28a158f3c7002cec1a Mon Sep 17 00:00:00 2001 From: Chatewgne Date: Thu, 23 Sep 2021 15:13:15 +0200 Subject: [PATCH 03/10] APIv2 : Add filter by practices on outdoor sites and children --- geotrek/api/tests/test_v2.py | 49 +++++++++++++++++++++++++++++++++++- geotrek/api/v2/filters.py | 20 +++++++++++---- geotrek/outdoor/models.py | 8 ++++++ 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/geotrek/api/tests/test_v2.py b/geotrek/api/tests/test_v2.py index dbf83d37af..d3a05e693f 100644 --- a/geotrek/api/tests/test_v2.py +++ b/geotrek/api/tests/test_v2.py @@ -3152,7 +3152,7 @@ def test_course_type_list_returns_published_in_language(self): self.assertIn(self.type_with_published_and_not_deleted_course_with_lang.pk, all_ids) -class SiteFilterByRatingsTestCase(BaseApiTest): +class OutdoorFilterByRatingsTestCase(BaseApiTest): """ Test filtering on ratings for outdoor course """ @@ -3234,3 +3234,50 @@ def test_course_list_ratings_filter2(self): all_ids.append(site['id']) self.assertNotIn(self.course1.pk, all_ids) self.assertIn(self.course2.pk, all_ids) + + +class OutdoorFilterBySuperPracticesTestCase(BaseApiTest): + """ Test filtering depending on published, deleted content for outdoor course types + """ + + @classmethod + def setUpTestData(cls): + cls.practice1 = outdoor_factory.PracticeFactory() + cls.practice2 = outdoor_factory.PracticeFactory() + cls.practice3 = outdoor_factory.PracticeFactory() + cls.practice3 = outdoor_factory.PracticeFactory() + cls.practice4 = outdoor_factory.PracticeFactory() + cls.site1 = outdoor_factory.SiteFactory(practice=cls.practice1) + cls.site2 = outdoor_factory.SiteFactory(practice=cls.practice2, parent=cls.site1) + cls.site3 = outdoor_factory.SiteFactory(practice=cls.practice3, parent=cls.site2) + cls.site4 = outdoor_factory.SiteFactory(practice=cls.practice4, parent=cls.site2) + + def test_filter_practice_in_tree_hierarchy(self): + """ Assert API returns only types with published course + """ + response = self.get_site_list({'practices_in_hierarchy': self.practice1.pk}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['count'], 1) + returned_types = response.json()['results'] + all_ids = [] + for type in returned_types: + all_ids.append(type['id']) + self.assertIn(self.site1.pk, all_ids) + self.assertNotIn(self.site2.pk, all_ids) + self.assertNotIn(self.site3.pk, all_ids) + self.assertNotIn(self.site4.pk, all_ids) + + def test_filter_practice_in_tree_hierarchy2(self): + """ Assert API returns only types with published course + """ + response = self.get_site_list({'practices_in_hierarchy': self.practice3.pk}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['count'], 3) + returned_types = response.json()['results'] + all_ids = [] + for type in returned_types: + all_ids.append(type['id']) + self.assertIn(self.site1.pk, all_ids) + self.assertIn(self.site2.pk, all_ids) + self.assertIn(self.site3.pk, all_ids) + self.assertNotIn(self.site4.pk, all_ids) diff --git a/geotrek/api/v2/filters.py b/geotrek/api/v2/filters.py index e7ce509052..1a7d0c9467 100644 --- a/geotrek/api/v2/filters.py +++ b/geotrek/api/v2/filters.py @@ -725,11 +725,13 @@ def filter_queryset(self, request, queryset, view): return queryset def get_schema_fields(self, view): - return Field( - name='ratings', required=False, location='query', schema=coreschema.String( - title=_("Query string"), - description=_('Filter by one or more ratings id, comma-separated.') - ) + return ( + Field( + name='ratings', required=False, location='query', schema=coreschema.Integer( + title=_("Ratings"), + description=_('Filter by one or more ratings id, comma-separated.') + ) + ), ) @@ -739,6 +741,14 @@ def filter_queryset(self, request, queryset, view): if root_sites_only: # Being a root node <=> having no parent queryset = queryset.filter(parent=None) + practices_in_hierachy = request.GET.get('practices_in_hierarchy') + if practices_in_hierachy: + wanted_practices = practices_in_hierachy.split(',') + for site in queryset: + # Exclude if practices in hierarchy don't match any wanted practices + matching_practices = site.super_practices.filter(id__in=wanted_practices) + if not matching_practices: + queryset = queryset.exclude(id=site.pk) q = request.GET.get('q') if q: queryset = queryset.filter(name__icontains=q) diff --git a/geotrek/outdoor/models.py b/geotrek/outdoor/models.py index 9e20656946..50ed0be943 100644 --- a/geotrek/outdoor/models.py +++ b/geotrek/outdoor/models.py @@ -236,6 +236,14 @@ def super_practices_display(self): return ", ".join(verbose) super_practices_verbose_name = _('Practices') + @property + def super_ratings(self): + "Return ratings of itself and its descendants" + ratings_id = self.get_descendants(include_self=True) \ + .exclude(ratings=None) \ + .values_list('ratings_id', flat=True) + return Rating.objects.filter(id__in=ratings_id) # Sorted and unique + @property def super_sectors(self): "Return sectors of itself and its descendants" From 41e0136bec06e20dd0dcf2a711e97a8f4ee67d88 Mon Sep 17 00:00:00 2001 From: Chatewgne Date: Thu, 23 Sep 2021 16:17:53 +0200 Subject: [PATCH 04/10] Display children ratings in Site page --- geotrek/outdoor/models.py | 14 +++++++++++++- .../templates/outdoor/site_detail_attributes.html | 6 +++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/geotrek/outdoor/models.py b/geotrek/outdoor/models.py index 50ed0be943..4300efbf8e 100644 --- a/geotrek/outdoor/models.py +++ b/geotrek/outdoor/models.py @@ -241,9 +241,21 @@ def super_ratings(self): "Return ratings of itself and its descendants" ratings_id = self.get_descendants(include_self=True) \ .exclude(ratings=None) \ - .values_list('ratings_id', flat=True) + .values_list('ratings', flat=True) return Rating.objects.filter(id__in=ratings_id) # Sorted and unique + @property + def super_ratings_display(self): + ratings = self.super_ratings + if not ratings: + return "" + verbose = [ + str(rating) if rating in self.ratings else "{}".format(escape(rating)) + for rating in ratings + ] + return ", ".join(verbose) + super_practices_verbose_name = _('Ratings') + @property def super_sectors(self): "Return sectors of itself and its descendants" diff --git a/geotrek/outdoor/templates/outdoor/site_detail_attributes.html b/geotrek/outdoor/templates/outdoor/site_detail_attributes.html index 8b63c5d7a1..4e082cc2c9 100644 --- a/geotrek/outdoor/templates/outdoor/site_detail_attributes.html +++ b/geotrek/outdoor/templates/outdoor/site_detail_attributes.html @@ -109,15 +109,15 @@

{% trans "Attributes" %}

{{ object|verbose:"advice" }} {% if object.advice %}{{ object.advice|safe }}{% else %}{% trans "None" %}{% endif %} - {% regroup object.ratings.all by scale as scales_list %} + {% regroup object.super_ratings by scale as scales_list %} {% for scale in scales_list%} {{ scale.grouper.name }} {% for rating in scale.list %} {% if not forloop.last %} - {{ rating.name }}, + {{ rating.name }}, {% else %} - {{ rating.name }} + {{ rating.name }} {% endif %} {% endfor %} From c10f106d7069bfa29edde7ed1f81234838f73dc6 Mon Sep 17 00:00:00 2001 From: Chatewgne Date: Thu, 23 Sep 2021 16:20:12 +0200 Subject: [PATCH 05/10] Add filters by practices in hierarchy and by ratings in hierarchy on Outdoor Sites --- docs/changelog.rst | 3 + geotrek/api/locale/fr/LC_MESSAGES/django.po | 6 ++ geotrek/api/tests/test_v2.py | 63 ++++++++++++++++----- geotrek/api/v2/filters.py | 26 ++++++++- 4 files changed, 80 insertions(+), 18 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7f2a363590..b750aa0ed0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,9 @@ CHANGELOG **New features** - APIv2 : Add filter by ratings on outdoor courses and sites +- APIv2 : Add filter by pratices in hierarchy on outdoor courses and sites +- APIv2 : Add filter by ratings in hierarchy on outdoor courses and sites +- Display children sites' ratings in site page 2.65.0 (2021-09-21) diff --git a/geotrek/api/locale/fr/LC_MESSAGES/django.po b/geotrek/api/locale/fr/LC_MESSAGES/django.po index 1041b1c808..f20d6763b9 100644 --- a/geotrek/api/locale/fr/LC_MESSAGES/django.po +++ b/geotrek/api/locale/fr/LC_MESSAGES/django.po @@ -404,3 +404,9 @@ msgstr "Sites racine seulement" msgid "Filter by one or more ratings id, comma-separated." msgstr "Filtrer par un ou plusieurs id de cotation, séparés par des virgules." + +msgid "Filter by one or more practices id, comma-separated. Return sites that have theses practices OR have at least one child site that does." +msgstr "Filtrer par un ou plusieurs id de pratique, séparés par des virgules. Retourne les sites qui ont ces pratiques OU des sites enfants avec ces pratiques." + +msgid "Filter by one or more ratings id, comma-separated. Return sites that have theses ratings OR have at least one child site that does." +msgstr "Filtrer par un ou plusieurs id de cotation, séparés par des virgules. Retourne les sites qui ont ces cotations OU des sites enfants avec ces cotations." \ No newline at end of file diff --git a/geotrek/api/tests/test_v2.py b/geotrek/api/tests/test_v2.py index d3a05e693f..d930a6f930 100644 --- a/geotrek/api/tests/test_v2.py +++ b/geotrek/api/tests/test_v2.py @@ -3171,8 +3171,6 @@ def setUpTestData(cls): cls.course2.ratings.set([cls.rating2]) def test_site_list_ratings_filter(self): - """ Assert API returns only sites with right ratings - """ response = self.get_site_list({'ratings': self.rating1.pk}) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()['count'], 1) @@ -3184,8 +3182,6 @@ def test_site_list_ratings_filter(self): self.assertNotIn(self.site2.pk, all_ids) def test_site_list_ratings_filter2(self): - """ Assert API returns only sites with right ratings - """ response = self.get_site_list({'ratings': self.rating2.pk}) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()['count'], 1) @@ -3197,8 +3193,6 @@ def test_site_list_ratings_filter2(self): self.assertNotIn(self.site1.pk, all_ids) def test_site_list_ratings_filter3(self): - """ Assert API returns only sites with right ratings - """ response = self.get_site_list({'ratings': f"{self.rating1.pk},{self.rating2.pk}"}) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()['count'], 2) @@ -3210,8 +3204,6 @@ def test_site_list_ratings_filter3(self): self.assertIn(self.site2.pk, all_ids) def test_course_list_ratings_filter(self): - """ Assert API returns only courses with right ratings - """ response = self.get_course_list({'ratings': f"{self.rating1.pk},{self.rating2.pk}"}) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()['count'], 2) @@ -3223,8 +3215,6 @@ def test_course_list_ratings_filter(self): self.assertIn(self.course2.pk, all_ids) def test_course_list_ratings_filter2(self): - """ Assert API returns only courses with right ratings - """ response = self.get_course_list({'ratings': self.rating2.pk}) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()['count'], 1) @@ -3237,7 +3227,7 @@ def test_course_list_ratings_filter2(self): class OutdoorFilterBySuperPracticesTestCase(BaseApiTest): - """ Test filtering depending on published, deleted content for outdoor course types + """ Test APIV2 filtering on ratings on sites """ @classmethod @@ -3253,8 +3243,6 @@ def setUpTestData(cls): cls.site4 = outdoor_factory.SiteFactory(practice=cls.practice4, parent=cls.site2) def test_filter_practice_in_tree_hierarchy(self): - """ Assert API returns only types with published course - """ response = self.get_site_list({'practices_in_hierarchy': self.practice1.pk}) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()['count'], 1) @@ -3268,8 +3256,6 @@ def test_filter_practice_in_tree_hierarchy(self): self.assertNotIn(self.site4.pk, all_ids) def test_filter_practice_in_tree_hierarchy2(self): - """ Assert API returns only types with published course - """ response = self.get_site_list({'practices_in_hierarchy': self.practice3.pk}) self.assertEqual(response.status_code, 200) self.assertEqual(response.json()['count'], 3) @@ -3281,3 +3267,50 @@ def test_filter_practice_in_tree_hierarchy2(self): self.assertIn(self.site2.pk, all_ids) self.assertIn(self.site3.pk, all_ids) self.assertNotIn(self.site4.pk, all_ids) + + +class OutdoorFilterBySuperRatingsTestCase(BaseApiTest): + """ Test APIV2 filtering on ratings on sites in hierarchy + """ + + @classmethod + def setUpTestData(cls): + cls.site1 = outdoor_factory.SiteFactory() + cls.rating_scale = outdoor_factory.RatingScaleFactory(practice=cls.site1.practice) + cls.rating1 = outdoor_factory.RatingFactory(scale=cls.rating_scale) + cls.rating2 = outdoor_factory.RatingFactory(scale=cls.rating_scale) + cls.rating3 = outdoor_factory.RatingFactory(scale=cls.rating_scale) + cls.rating4 = outdoor_factory.RatingFactory(scale=cls.rating_scale) + cls.site2 = outdoor_factory.SiteFactory(parent=cls.site1) + cls.site3 = outdoor_factory.SiteFactory(parent=cls.site2) + cls.site4 = outdoor_factory.SiteFactory(parent=cls.site2) + cls.site1.ratings.set([cls.rating1]) + cls.site2.ratings.set([cls.rating2]) + cls.site3.ratings.set([cls.rating3]) + cls.site4.ratings.set([cls.rating4]) + + def test_filter_ratings_in_tree_hierarchy(self): + response = self.get_site_list({'ratings_in_hierarchy': self.rating1.pk}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['count'], 1) + returned_types = response.json()['results'] + all_ids = [] + for type in returned_types: + all_ids.append(type['id']) + self.assertIn(self.site1.pk, all_ids) + self.assertNotIn(self.site2.pk, all_ids) + self.assertNotIn(self.site3.pk, all_ids) + self.assertNotIn(self.site4.pk, all_ids) + + def test_filter_ratings_in_tree_hierarchy2(self): + response = self.get_site_list({'ratings_in_hierarchy': self.rating3.pk}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['count'], 3) + returned_types = response.json()['results'] + all_ids = [] + for type in returned_types: + all_ids.append(type['id']) + self.assertIn(self.site1.pk, all_ids) + self.assertIn(self.site2.pk, all_ids) + self.assertIn(self.site3.pk, all_ids) + self.assertNotIn(self.site4.pk, all_ids) diff --git a/geotrek/api/v2/filters.py b/geotrek/api/v2/filters.py index 1a7d0c9467..8bdf622c6e 100644 --- a/geotrek/api/v2/filters.py +++ b/geotrek/api/v2/filters.py @@ -741,14 +741,22 @@ def filter_queryset(self, request, queryset, view): if root_sites_only: # Being a root node <=> having no parent queryset = queryset.filter(parent=None) - practices_in_hierachy = request.GET.get('practices_in_hierarchy') - if practices_in_hierachy: - wanted_practices = practices_in_hierachy.split(',') + practices_in_hierarchy = request.GET.get('practices_in_hierarchy') + if practices_in_hierarchy: + wanted_practices = practices_in_hierarchy.split(',') for site in queryset: # Exclude if practices in hierarchy don't match any wanted practices matching_practices = site.super_practices.filter(id__in=wanted_practices) if not matching_practices: queryset = queryset.exclude(id=site.pk) + ratings_in_hierarchy = request.GET.get('ratings_in_hierarchy') + if ratings_in_hierarchy: + wanted_ratings = ratings_in_hierarchy.split(',') + for site in queryset: + # Exclude if ratings in hierarchy don't match any wanted ratings + matching_ratings = site.super_ratings.filter(id__in=wanted_ratings) + if not matching_ratings: + queryset = queryset.exclude(id=site.pk) q = request.GET.get('q') if q: queryset = queryset.filter(name__icontains=q) @@ -768,6 +776,18 @@ def get_schema_fields(self, view): description=_('Only return sites that are at the top of the hierarchy and have no parent. Use any string to activate.') ) ), + Field( + name='practices_in_hierarchy', required=False, location='query', schema=coreschema.Integer( + title=_("Practices in hierarchy"), + description=_('Filter by one or more practices id, comma-separated. Return sites that have theses practices OR have at least one child site that does.') + ) + ), + Field( + name='ratings_in_hierarchy', required=False, location='query', schema=coreschema.Integer( + title=_("Ratings in hierarchy"), + description=_('Filter by one or more ratings id, comma-separated. Return sites that have theses ratings OR have at least one child site that does.') + ) + ), ) From 41962646b4903be3854ff9f7ccd25baacafacb42 Mon Sep 17 00:00:00 2001 From: Chatewgne Date: Thu, 23 Sep 2021 17:35:48 +0200 Subject: [PATCH 06/10] Remove unused method --- geotrek/outdoor/models.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/geotrek/outdoor/models.py b/geotrek/outdoor/models.py index 4300efbf8e..c0adab42c3 100644 --- a/geotrek/outdoor/models.py +++ b/geotrek/outdoor/models.py @@ -244,18 +244,6 @@ def super_ratings(self): .values_list('ratings', flat=True) return Rating.objects.filter(id__in=ratings_id) # Sorted and unique - @property - def super_ratings_display(self): - ratings = self.super_ratings - if not ratings: - return "" - verbose = [ - str(rating) if rating in self.ratings else "{}".format(escape(rating)) - for rating in ratings - ] - return ", ".join(verbose) - super_practices_verbose_name = _('Ratings') - @property def super_sectors(self): "Return sectors of itself and its descendants" From bf0332f193acdd84d0de1e9caee082eef6e24bc7 Mon Sep 17 00:00:00 2001 From: Chatewgne Date: Fri, 24 Sep 2021 10:25:05 +0200 Subject: [PATCH 07/10] Improve performances for ratings and practices APIv2 filters --- geotrek/api/tests/test_v2.py | 13 +++++++++++++ geotrek/api/v2/filters.py | 13 +++++++------ geotrek/outdoor/models.py | 23 +++++++++++++++++------ 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/geotrek/api/tests/test_v2.py b/geotrek/api/tests/test_v2.py index a16135c788..d019bcc65e 100644 --- a/geotrek/api/tests/test_v2.py +++ b/geotrek/api/tests/test_v2.py @@ -3337,3 +3337,16 @@ def test_filter_ratings_in_tree_hierarchy2(self): self.assertIn(self.site2.pk, all_ids) self.assertIn(self.site3.pk, all_ids) self.assertNotIn(self.site4.pk, all_ids) + + def test_filter_ratings_in_tree_hierarchy3(self): + response = self.get_site_list({'ratings_in_hierarchy': f"{self.rating3.pk}, {self.rating4.pk}"}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['count'], 4) + returned_types = response.json()['results'] + all_ids = [] + for type in returned_types: + all_ids.append(type['id']) + self.assertIn(self.site1.pk, all_ids) + self.assertIn(self.site2.pk, all_ids) + self.assertIn(self.site3.pk, all_ids) + self.assertIn(self.site4.pk, all_ids) diff --git a/geotrek/api/v2/filters.py b/geotrek/api/v2/filters.py index 99a858f99f..288a5761ca 100644 --- a/geotrek/api/v2/filters.py +++ b/geotrek/api/v2/filters.py @@ -743,19 +743,20 @@ def filter_queryset(self, request, queryset, view): queryset = queryset.filter(parent=None) practices_in_hierarchy = request.GET.get('practices_in_hierarchy') if practices_in_hierarchy: - wanted_practices = practices_in_hierarchy.split(',') + wanted_practices = set(map(int, practices_in_hierarchy.split(','))) + queryset = queryset.filter('super_practices__id_in=wabted') for site in queryset: # Exclude if practices in hierarchy don't match any wanted practices - matching_practices = site.super_practices.filter(id__in=wanted_practices) - if not matching_practices: + found_practices = site.super_practices_id + if found_practices.isdisjoint(wanted_practices): queryset = queryset.exclude(id=site.pk) ratings_in_hierarchy = request.GET.get('ratings_in_hierarchy') if ratings_in_hierarchy: - wanted_ratings = ratings_in_hierarchy.split(',') + wanted_ratings = set(map(int, ratings_in_hierarchy.split(','))) for site in queryset: # Exclude if ratings in hierarchy don't match any wanted ratings - matching_ratings = site.super_ratings.filter(id__in=wanted_ratings) - if not matching_ratings: + found_ratings = site.super_ratings_id + if found_ratings.isdisjoint(wanted_ratings): queryset = queryset.exclude(id=site.pk) q = request.GET.get('q') if q: diff --git a/geotrek/outdoor/models.py b/geotrek/outdoor/models.py index 4906be6bad..5cd617599f 100644 --- a/geotrek/outdoor/models.py +++ b/geotrek/outdoor/models.py @@ -217,12 +217,17 @@ def published_children(self): return self.children.filter(q) @property - def super_practices(self): - "Return practices of itself and its descendants" + def super_practices_id(self): + "Return practices of itself and its descendants as ids" practices_id = self.get_descendants(include_self=True) \ .exclude(practice=None) \ .values_list('practice_id', flat=True) - return Practice.objects.filter(id__in=practices_id) # Sorted and unique + return set(practices_id) + + @property + def super_practices(self): + "Return practices of itself and its descendants as objects" + return Practice.objects.filter(id__in=self.super_practices_id) # Sorted and unique @property def super_practices_display(self): @@ -237,12 +242,18 @@ def super_practices_display(self): super_practices_verbose_name = _('Practices') @property - def super_ratings(self): - "Return ratings of itself and its descendants" + def super_ratings_id(self): + "Return ratings of itself and its descendants as ids" ratings_id = self.get_descendants(include_self=True) \ .exclude(ratings=None) \ .values_list('ratings', flat=True) - return Rating.objects.filter(id__in=ratings_id) # Sorted and unique + return set(ratings_id) + + @property + def super_ratings(self): + "Return ratings of itself and its descendants as objects" + return Rating.objects.filter(id__in=self.super_ratings_id) # Sorted and unique + @property def super_sectors(self): From bff61f66198d324be300455041de170626cd3ef3 Mon Sep 17 00:00:00 2001 From: Chatewgne Date: Fri, 24 Sep 2021 10:26:35 +0200 Subject: [PATCH 08/10] Codestyle --- geotrek/outdoor/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/geotrek/outdoor/models.py b/geotrek/outdoor/models.py index 5cd617599f..20fa3f927b 100644 --- a/geotrek/outdoor/models.py +++ b/geotrek/outdoor/models.py @@ -254,7 +254,6 @@ def super_ratings(self): "Return ratings of itself and its descendants as objects" return Rating.objects.filter(id__in=self.super_ratings_id) # Sorted and unique - @property def super_sectors(self): "Return sectors of itself and its descendants" From 7ad3e447c23c5b436a81b6715812a284cd4d3562 Mon Sep 17 00:00:00 2001 From: Chatewgne Date: Fri, 24 Sep 2021 11:14:07 +0200 Subject: [PATCH 09/10] Remove test line --- geotrek/api/v2/filters.py | 1 - 1 file changed, 1 deletion(-) diff --git a/geotrek/api/v2/filters.py b/geotrek/api/v2/filters.py index 288a5761ca..9763869f2b 100644 --- a/geotrek/api/v2/filters.py +++ b/geotrek/api/v2/filters.py @@ -744,7 +744,6 @@ def filter_queryset(self, request, queryset, view): practices_in_hierarchy = request.GET.get('practices_in_hierarchy') if practices_in_hierarchy: wanted_practices = set(map(int, practices_in_hierarchy.split(','))) - queryset = queryset.filter('super_practices__id_in=wabted') for site in queryset: # Exclude if practices in hierarchy don't match any wanted practices found_practices = site.super_practices_id From 3808f6e06f755657f9843662fefff68433e9ca74 Mon Sep 17 00:00:00 2001 From: Chatewgne Date: Fri, 24 Sep 2021 14:46:51 +0200 Subject: [PATCH 10/10] Add comment to alert on performances --- geotrek/api/v2/filters.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/geotrek/api/v2/filters.py b/geotrek/api/v2/filters.py index 9763869f2b..a9edd8174a 100644 --- a/geotrek/api/v2/filters.py +++ b/geotrek/api/v2/filters.py @@ -742,6 +742,7 @@ def filter_queryset(self, request, queryset, view): # Being a root node <=> having no parent queryset = queryset.filter(parent=None) practices_in_hierarchy = request.GET.get('practices_in_hierarchy') + # TODO Optimize this filter by finding an alternative to queryset iterating if practices_in_hierarchy: wanted_practices = set(map(int, practices_in_hierarchy.split(','))) for site in queryset: @@ -749,6 +750,7 @@ def filter_queryset(self, request, queryset, view): found_practices = site.super_practices_id if found_practices.isdisjoint(wanted_practices): queryset = queryset.exclude(id=site.pk) + # TODO Optimize this filter by finding an alternative to queryset iterating ratings_in_hierarchy = request.GET.get('ratings_in_hierarchy') if ratings_in_hierarchy: wanted_ratings = set(map(int, ratings_in_hierarchy.split(',')))