diff --git a/apps/collection/models.py b/apps/collection/models.py index d1f90381..70e13095 100644 --- a/apps/collection/models.py +++ b/apps/collection/models.py @@ -1,21 +1,21 @@ import re +from django.conf import settings from django.contrib.contenttypes.fields import ContentType from django.contrib.postgres.fields import JSONField from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from django.conf import settings from django.utils.translation import gettext_lazy as _ from mptt.models import MPTTModel, TreeForeignKey from slugify import slugify +from collection import tasks from establishment.models import Establishment from location.models import Country, Region, WineRegion, WineSubRegion, City from product.models import Product from review.models import Review -from collection import tasks from translation.models import Language -from utils.methods import transform_into_readable_str +from utils.methods import transform_into_section_name from utils.models import ( ProjectBaseMixin, TJSONField, TranslatedFieldsMixin, URLImageMixin, IntermediateGalleryModelMixin @@ -177,9 +177,13 @@ class GuideQuerySet(models.QuerySet): return self.with_base_related().prefetch_related('guideelement_set') def by_country_id(self, country_id): - """Return QuerySet filtered by country code.""" + """Return QuerySet filtered by country id.""" return self.filter(country_json__id__contains=country_id) + def by_country_code(self, country_code): + """Return QuerySet filtered by country code.""" + return self.filter(site__country__code=country_code) + def annotate_in_restaurant_section(self): """Annotate flag if GuideElement in RestaurantSectionNode.""" restaurant_guides = models.Subquery( @@ -420,6 +424,32 @@ class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin): filter_set=self.guidefilter.product_filter_set, ) + def extend_establishment_guide(self, establishment_id: int): + if self.guide_type in [self.ARTISAN, self.RESTAURANT]: + if settings.USE_CELERY: + tasks.populate_establishment_guide.delay( + guide_id=self.id, + establishment_id=establishment_id, + ) + else: + tasks.populate_establishment_guide( + guide_id=self.id, + establishment_id=establishment_id, + ) + + def remove_establishment(self, establishment_id: int): + if self.guide_type in [self.ARTISAN, self.RESTAURANT]: + if settings.USE_CELERY: + tasks.remove_establishment_guide.delay( + guide_id=self.id, + establishment_id=establishment_id, + ) + else: + tasks.remove_establishment_guide( + guide_id=self.id, + establishment_id=establishment_id, + ) + def regenerate_elements(self): # get Root node root_node = GuideElement.objects.get_root_node(self) @@ -905,7 +935,7 @@ class GuideElementManager(models.Manager): parent_node_qs = GuideElement.objects.filter(id=yard_node_id) if not wine_color_name.endswith('SectionNode'): - wine_color_name = transform_into_readable_str(wine_color_name) + wine_color_name = transform_into_section_name(wine_color_name) wine_color_section, _ = GuideWineColorSection.objects.get_or_create( name=wine_color_name, @@ -972,6 +1002,14 @@ class GuideElementQuerySet(models.QuerySet): """Return QuerySet with descendants.""" return self.exclude(guide_element_type__name='Root') + def by_establishment_type(self, establishment_type_index_name: str): + """Return QuerySet by establishment_type.""" + return self.filter( + guide_element_type__name__iexact=transform_into_section_name( + establishment_type_index_name + ) + ) + class GuideElement(ProjectBaseMixin, MPTTModel): """Frozen state of elements of guide instance.""" diff --git a/apps/collection/tasks.py b/apps/collection/tasks.py index b08a61d0..24ac6686 100644 --- a/apps/collection/tasks.py +++ b/apps/collection/tasks.py @@ -2,7 +2,8 @@ import logging from celery import shared_task -from utils.methods import transform_into_readable_str + +from utils.methods import transform_into_section_name logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO) logger = logging.getLogger(__name__) @@ -31,7 +32,7 @@ def get_additional_product_data(section_node, product): @shared_task def generate_establishment_guide_elements(guide_id: int, filter_set: dict): """Generate guide elements.""" - from collection.models import GuideElement, Guide + from collection.models import Guide from establishment.models import Establishment guide = Guide.objects.get(id=guide_id) @@ -39,35 +40,53 @@ def generate_establishment_guide_elements(guide_id: int, filter_set: dict): queryset_values = Establishment.objects.filter(**filter_set).values() try: for instance in queryset_values: - establishment_id = instance.get('id') - establishment_qs = Establishment.objects.filter(id=establishment_id) - if establishment_qs.exists(): - establishment = establishment_qs.first() - root_node, _ = GuideElement.objects.get_or_create_root_node(guide_id) - if root_node: - city_node, _ = GuideElement.objects.get_or_create_city_node(root_node.id, - establishment.address.city_id) - if city_node: - section_node, _ = GuideElement.objects.get_or_create_establishment_section_node( - city_node.id, - transform_into_readable_str(establishment.establishment_type.index_name), - ) - if section_node: - GuideElement.objects.get_or_create_establishment_node( - *get_additional_establishment_data(section_node=section_node, - establishment=establishment)) - else: - logger.error( - f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n' - f'DETAIL: Guide ID {guide_id} - SectionNode is not exists.') + populate_establishment_guide(guide_id, instance.get('id')) + except Exception as e: + guide.change_state(Guide.WAITING) + logger.error(f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n' + f'DETAIL: Guide ID {guide_id} - {e}') + else: + guide.update_count_related_objects() + guide.change_state(Guide.BUILT) + + +@shared_task +def populate_establishment_guide(guide_id: int, establishment_id: int): + """Extend guide.""" + from collection.models import GuideElement, Guide + from establishment.models import Establishment + + guide = Guide.objects.get(id=guide_id) + guide.change_state(Guide.BUILDING) + try: + establishment_qs = Establishment.objects.filter(id=establishment_id) + if establishment_qs.exists(): + establishment = establishment_qs.first() + root_node, _ = GuideElement.objects.get_or_create_root_node(guide_id) + if root_node: + city_node, _ = GuideElement.objects.get_or_create_city_node(root_node.id, + establishment.address.city_id) + if city_node: + section_node, _ = GuideElement.objects.get_or_create_establishment_section_node( + city_node.id, + transform_into_section_name(establishment.establishment_type.index_name), + ) + if section_node: + GuideElement.objects.get_or_create_establishment_node( + *get_additional_establishment_data(section_node=section_node, + establishment=establishment)) else: - logger.error(f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n' - f'DETAIL: Guide ID {guide_id} - CityNode is not exists.') + logger.error( + f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n' + f'DETAIL: Guide ID {guide_id} - SectionNode is not exists.') else: logger.error(f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n' - f'DETAIL: Guide ID {guide_id} - RootNode is not exists.') + f'DETAIL: Guide ID {guide_id} - CityNode is not exists.') else: logger.error(f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n' + f'DETAIL: Guide ID {guide_id} - RootNode is not exists.') + else: + logger.error(f'METHOD_NAME: {generate_establishment_guide_elements.__name__}\n' f'DETAIL: Guide ID {guide_id} - Establishment {establishment_id} id is not exists.') except Exception as e: guide.change_state(Guide.WAITING) @@ -78,6 +97,42 @@ def generate_establishment_guide_elements(guide_id: int, filter_set: dict): guide.change_state(Guide.BUILT) +@shared_task +def remove_establishment_guide(guide_id: int, establishment_id: int): + """Extend guide.""" + from collection.models import GuideElement, Guide + from establishment.models import Establishment + + guide = Guide.objects.get(id=guide_id) + guide.change_state(Guide.REMOVING) + try: + establishment_qs = Establishment.objects.filter(id=establishment_id) + if establishment_qs.exists(): + establishment = establishment_qs.first() + if hasattr(establishment, 'establishment_type') and establishment.establishment_type.index_name: + establishment_node_qs = GuideElement.objects.filter(establishment=establishment, + guide=guide) + if establishment_node_qs.exists(): + establishment_node_qs.first().delete() + else: + logger.error(f'METHOD_NAME: {remove_establishment_guide.__name__}\n' + f'DETAIL: Guide ID {guide_id} - EstablishmentNode {establishment_id} id is not exists.') + else: + logger.error(f'METHOD_NAME: {remove_establishment_guide.__name__}\n' + f'DETAIL: Guide ID {guide_id} - Establishment {establishment_id} ' + f'has not establishment type or establishment type index name.') + else: + logger.error(f'METHOD_NAME: {remove_establishment_guide.__name__}\n' + f'DETAIL: Guide ID {guide_id} - Establishment {establishment_id} id is not exists.') + except Exception as e: + guide.change_state(Guide.WAITING) + logger.error(f'METHOD_NAME: {remove_establishment_guide.__name__}\n' + f'DETAIL: Guide ID {guide_id} - {e}') + else: + guide.update_count_related_objects() + guide.change_state(Guide.BUILT) + + @shared_task def generate_product_guide_elements(guide_id: int, filter_set: dict): """Generate guide elements.""" diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index ffdfb578..2211bbad 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from account.serializers.common import UserShortSerializer +from collection.models import Guide from establishment import models, serializers as model_serializers from establishment.models import ContactEmail, ContactPhone, EstablishmentEmployee from gallery.models import Image @@ -582,6 +583,17 @@ class EstablishmentAdminListSerializer(UserShortSerializer): ] +class EstablishmentGuideSerializer(serializers.ModelSerializer): + """Serializer for model Guide. """ + + class Meta: + """Meta class.""" + model = Guide + fields = [ + 'id', + ] + + class EstablishmentEmployeePositionsSerializer(serializers.ModelSerializer): """Establishments from employee serializer""" diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index 177bc68b..2a21f281 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -30,6 +30,8 @@ urlpatterns = [ name='note-rud'), path('slug//admin/', views.EstablishmentAdminView.as_view(), name='establishment-admin-list'), + path('slug//guides//', views.EstablishmentGuideCreateDestroyView.as_view(), + name='guide-list-create'), path('menus/dishes/', views.MenuDishesListCreateView.as_view(), name='menu-dishes-list'), path('menus/dishes//', views.MenuDishesRUDView.as_view(), name='menu-dishes-rud'), path('menus/dishes//gallery/', views.MenuGalleryListView.as_view(), name='menu-dishes-gallery-list'), diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index c47f5468..e7106245 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -1,6 +1,6 @@ """Establishment app views.""" -from django.db.models.query_utils import Q from django.db import transaction +from django.db.models.query_utils import Q from django.http import Http404 from django.shortcuts import get_object_or_404 from django_filters.rest_framework import DjangoFilterBackend @@ -8,6 +8,7 @@ from rest_framework import generics, response, status from rest_framework.response import Response from account.models import User +from collection.models import Guide from establishment import filters, models, serializers from establishment.models import EstablishmentEmployee, Menu from timetable.models import Timetable @@ -616,6 +617,82 @@ class EstablishmentAdminView(generics.ListAPIView): return User.objects.establishment_admin(establishment).distinct() +class EstablishmentGuideCreateDestroyView(generics.GenericAPIView): + """Add/Remove establishment from guide.""" + establishment_lookup_url_kwarg = 'slug' + guide_lookup_url_kwarg = 'guide_id' + permission_classes = get_permission_classes( + IsEstablishmentManager, + IsEstablishmentAdministrator + ) + + def get_establishment_queryset(self): + """Get Establishment queryset.""" + return EstablishmentMixinViews.get_queryset(self) + + def get_guide_queryset(self): + """Get Guide queryset.""" + queryset = Guide.objects + if hasattr(self, 'request') and hasattr(self.request, 'country_code'): + return queryset.by_country_code(self.request.country_code) + return queryset.none() + + def get_establishment(self): + queryset = self.get_establishment_queryset() + + # Perform the lookup filtering. + lookup_url_kwarg = getattr(self, 'establishment_lookup_url_kwarg', None) + + assert lookup_url_kwarg and lookup_url_kwarg in self.kwargs, ( + 'Expected view %s to be called with a URL keyword argument ' + 'named "%s". Fix your URL conf, or set the `.lookup_field` ' + 'attribute on the view correctly.' % + (self.__class__.__name__, lookup_url_kwarg) + ) + + filters = {'klass': queryset, lookup_url_kwarg: self.kwargs.get(lookup_url_kwarg)} + obj = get_object_or_404(**filters) + + # May raise a permission denied + self.check_object_permissions(self.request, obj) + + return obj + + def get_guide(self): + queryset = self.get_guide_queryset() + + # Perform the lookup filtering. + lookup_url_kwarg = getattr(self, 'guide_lookup_url_kwarg', None) + + assert lookup_url_kwarg and lookup_url_kwarg in self.kwargs, ( + 'Expected view %s to be called with a URL keyword argument ' + 'named "%s". Fix your URL conf, or set the `.lookup_field` ' + 'attribute on the view correctly.' % + (self.__class__.__name__, lookup_url_kwarg) + ) + + obj = get_object_or_404(klass=queryset, id=self.kwargs.get(lookup_url_kwarg)) + + # May raise a permission denied + self.check_object_permissions(self.request, obj) + + return obj + + def post(self, request, *args, **kwargs): + """Implement GET-method.""" + establishment = self.get_establishment() + guide = self.get_guide() + guide.extend_establishment_guide(establishment.id) + return Response(status=status.HTTP_200_OK) + + def delete(self, request, *args, **kwargs): + """Implement DELETE-method.""" + establishment = self.get_establishment() + guide = self.get_guide() + guide.remove_establishment(establishment.id) + return Response(status=status.HTTP_204_NO_CONTENT) + + class MenuDishesListCreateView(generics.ListCreateAPIView): """Menu (dessert, main_course, starter) list create view.""" serializer_class = serializers.MenuDishesSerializer diff --git a/apps/utils/methods.py b/apps/utils/methods.py index 73310fd4..37b5068b 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -1,12 +1,13 @@ """Utils app method.""" import logging +import pathlib import random import re import string from collections import namedtuple from functools import reduce from io import BytesIO -import pathlib + import requests from PIL import Image from django.conf import settings @@ -169,11 +170,11 @@ def dictfetchall(cursor): ] -def transform_into_readable_str(raw_string: str, postfix: str = 'SectionNode'): +def transform_into_readable_str(raw_string: str): """ Transform slug into section name, i.e: like - "EffervescentRoseDeSaigneeSectionNode" + "EffervescentRoseDeSaignee" from "effervescent-rose-de-saignee" """ @@ -183,6 +184,17 @@ def transform_into_readable_str(raw_string: str, postfix: str = 'SectionNode'): return f"{''.join([i.capitalize() for i in result])}" +def transform_into_section_name(raw_string: str, postfix: str = 'SectionNode'): + """ + Transform slug into section name, i.e: + like + "EffervescentRoseDeSaigneeSectionNode" + from + "effervescent-rose-de-saignee" + """ + return f'{transform_into_readable_str(raw_string)}{postfix}' + + def transform_camelcase_to_underscore(raw_string: str): """ Transform str, i.e: