diff --git a/apps/collection/filters.py b/apps/collection/filters.py new file mode 100644 index 00000000..66b5e454 --- /dev/null +++ b/apps/collection/filters.py @@ -0,0 +1,56 @@ +"""Collection app filters.""" +from django_filters import rest_framework as filters +from django.core.validators import EMPTY_VALUES + +from collection import models + + +class CollectionFilterSet(filters.FilterSet): + """Collection filter set.""" + establishment_id = filters.NumberFilter( + field_name='establishments__id', + help_text='Establishment id. Allows to filter list of collections by choosen estblishment. ' + 'Use for Establishment detail\'s sheet to content display within ' + '"Collections & Guides" tab.' + ) + + # "ordering" instead of "o" is for backward compatibility + ordering = filters.OrderingFilter( + # tuple-mapping retains order + fields=( + ('rank', 'rank'), + ('start', 'start'), + ), + help_text='Ordering by fields - rank, start', + ) + + class Meta: + """Meta class.""" + model = models.Collection + fields = ( + 'ordering', + 'establishment_id', + ) + + +class GuideFilterSet(filters.FilterSet): + """Guide filter set.""" + establishment_id = filters.NumberFilter( + method='by_establishment_id', + help_text='Establishment id. Allows to filter list of guides by choosen establishment. ' + 'Use for Establishment detail\'s sheet to content display within ' + '"Collections & Guides" tab.' + ) + + class Meta: + """Meta class.""" + model = models.Guide + fields = ( + 'establishment_id', + ) + + def by_establishment_id(self, queryset, name, value): + """Filter by establishment id.""" + if value not in EMPTY_VALUES: + return queryset.by_establishment_id(value) + return queryset diff --git a/apps/collection/models.py b/apps/collection/models.py index 91173ed0..2fe28e4f 100644 --- a/apps/collection/models.py +++ b/apps/collection/models.py @@ -171,17 +171,26 @@ class GuideQuerySet(models.QuerySet): """Return QuerySet with related.""" return self.select_related('site', ) + def with_extended_related(self): + """Return QuerySet with extended related.""" + return self.with_base_related().prefetch_related('guideelement_set') + def by_country_id(self, country_id): """Return QuerySet filtered by country code.""" return self.filter(country_json__id__contains=country_id) def annotate_in_restaurant_section(self): """Annotate flag if GuideElement in RestaurantSectionNode.""" + restaurant_guides = models.Subquery( + self.filter( + guideelement__guide_element_type__name='EstablishmentNode', + guideelement__parent__guide_element_type__name='RestaurantSectionNode', + ).values_list('id', flat=True).distinct() + ) return self.annotate( in_restaurant_section=models.Case( models.When( - guideelement__guide_element_type__name='EstablishmentNode', - guideelement__parent__guide_element_type__name='RestaurantSectionNode', + id__in=restaurant_guides, then=True), default=False, output_field=models.BooleanField(default=False) @@ -190,11 +199,16 @@ class GuideQuerySet(models.QuerySet): def annotate_in_shop_section(self): """Annotate flag if GuideElement in ShopSectionNode.""" + shop_guides = models.Subquery( + self.filter( + guideelement__guide_element_type__name='EstablishmentNode', + guideelement__parent__guide_element_type__name='ShopSectionNode', + ).values_list('guideelement__id', flat=True).distinct() + ) return self.annotate( in_shop_section=models.Case( models.When( - guideelement__guide_element_type__name='EstablishmentNode', - guideelement__parent__guide_element_type__name='ShopSectionNode', + id__in=shop_guides, then=True), default=False, output_field=models.BooleanField(default=False) @@ -205,37 +219,60 @@ class GuideQuerySet(models.QuerySet): """Return QuerySet with annotated field - restaurant_counter.""" return self.annotate_in_restaurant_section().annotate( restaurant_counter=models.Count( - 'guideelement', + 'guideelement__establishment', filter=models.Q(in_restaurant_section=True) & - models.Q(guideelement__parent_id__isnull=False), - distinct=True)) + models.Q(guideelement__parent_id__isnull=True), + distinct=True + ) + ) def annotate_shop_counter(self): """Return QuerySet with annotated field - shop_counter.""" return self.annotate_in_shop_section().annotate( shop_counter=models.Count( - 'guideelement', + 'guideelement__establishment', filter=models.Q(in_shop_section=True) & - models.Q(guideelement__parent_id__isnull=False), - distinct=True)) + models.Q(guideelement__parent_id__isnull=True), + distinct=True + ) + ) def annotate_wine_counter(self): """Return QuerySet with annotated field - shop_counter.""" return self.annotate_in_restaurant_section().annotate( wine_counter=models.Count( - 'guideelement', + 'guideelement__product', filter=models.Q(guideelement__guide_element_type__name='WineNode') & models.Q(guideelement__parent_id__isnull=False), - distinct=True)) + distinct=True + ) + ) def annotate_present_objects_counter(self): """Return QuerySet with annotated field - present_objects_counter.""" - return self.annotate_in_restaurant_section().annotate( - present_objects_counter=models.Count( - 'guideelement', - filter=models.Q(guideelement__guide_element_type__name__in=['EstablishmentNode', 'WineNode']) & - models.Q(guideelement__parent_id__isnull=False), - distinct=True)) + return ( + self.annotate_restaurant_counter() + .annotate_shop_counter() + .annotate_wine_counter() + .annotate( + present_objects_counter=( + models.F('restaurant_counter') + + models.F('shop_counter') + + models.F('wine_counter') + ) + ) + ) + + def annotate_counters(self): + return ( + self.annotate_restaurant_counter() + .annotate_shop_counter() + .annotate_wine_counter() + .annotate_present_objects_counter() + ) + + def by_establishment_id(self, establishment_id: int): + return self.filter(guideelement__establishment=establishment_id).distinct() class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin): diff --git a/apps/collection/serializers/common.py b/apps/collection/serializers/common.py index da33d271..8943c7a2 100644 --- a/apps/collection/serializers/common.py +++ b/apps/collection/serializers/common.py @@ -109,6 +109,7 @@ class GuideBaseSerializer(serializers.ModelSerializer): restaurant_counter = serializers.IntegerField(read_only=True) shop_counter = serializers.IntegerField(read_only=True) wine_counter = serializers.IntegerField(read_only=True) + present_objects_counter = serializers.IntegerField(read_only=True) count_objects_during_init = serializers.IntegerField(read_only=True, source='count_related_objects') @@ -131,6 +132,7 @@ class GuideBaseSerializer(serializers.ModelSerializer): 'restaurant_counter', 'shop_counter', 'wine_counter', + 'present_objects_counter', 'count_objects_during_init', ] extra_kwargs = { diff --git a/apps/collection/views/back.py b/apps/collection/views/back.py index 73fb7b18..8b9d0d8e 100644 --- a/apps/collection/views/back.py +++ b/apps/collection/views/back.py @@ -1,13 +1,11 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ -from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics from rest_framework import mixins, permissions, viewsets from rest_framework import status -from rest_framework.filters import OrderingFilter from rest_framework.response import Response -from collection import models, serializers +from collection import models, serializers, filters from collection import tasks from utils.views import BindObjectMixin @@ -34,12 +32,7 @@ class GuideBaseView(generics.GenericAPIView): def get_queryset(self): """Overridden get_queryset method.""" - return models.Guide.objects.with_base_related() \ - .annotate_restaurant_counter() \ - .annotate_shop_counter() \ - .annotate_wine_counter() \ - .annotate_present_objects_counter() \ - .distinct() + return models.Guide.objects.with_extended_related().annotate_counters() class GuideFilterBaseView(generics.GenericAPIView): @@ -72,17 +65,14 @@ class CollectionBackOfficeViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, BindObjectMixin, CollectionViewSet): - """ViewSet for Collection model for BackOffice users.""" + """ViewSet for Collections list for BackOffice users and Collection create.""" permission_classes = (permissions.IsAuthenticated,) - queryset = models.Collection.objects.with_base_related() - filter_backends = [DjangoFilterBackend, OrderingFilter] + queryset = models.Collection.objects.with_base_related().order_by('-start') + filter_class = filters.CollectionFilterSet serializer_class = serializers.CollectionBackOfficeSerializer bind_object_serializer_class = serializers.CollectionBindObjectSerializer - ordering_fields = ('rank', 'start') - ordering = ('-start', ) - def perform_binding(self, serializer): data = serializer.validated_data collection = data.pop('collection') @@ -106,7 +96,8 @@ class CollectionBackOfficeViewSet(mixins.CreateModelMixin, class GuideListCreateView(GuideBaseView, generics.ListCreateAPIView): - """View for Guide model for BackOffice users.""" + """View for Guides list for BackOffice users and Guide create.""" + filter_class = filters.GuideFilterSet class GuideFilterCreateView(GuideFilterBaseView, diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index b52c909f..86d5b787 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -1,8 +1,7 @@ """Establishment app filters.""" from django.core.validators import EMPTY_VALUES from django.utils.translation import ugettext_lazy as _ -from django_filters import rest_framework as filters, Filter -from django_filters.fields import Lookup +from django_filters import rest_framework as filters from rest_framework.serializers import ValidationError from establishment import models diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 0bb13cf6..25f2b14f 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -15,7 +15,7 @@ urlpatterns = [ name='create-comment'), path('slug//comments//', views.EstablishmentCommentRUDView.as_view(), name='rud-comment'), - path('slug//favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), + path('slug//collections/', views.EstablishmentFavoritesCreateDestroyView.as_view(), name='create-destroy-favorites'), # similar establishments by type/subtype