diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 6cf13807..f86717ed 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -14,26 +14,24 @@ from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.search import TrigramSimilarity from django.core.exceptions import ValidationError -from django.core.validators import MinValueValidator, MaxValueValidator +from django.core.validators import MaxValueValidator, MinValueValidator, FileExtensionValidator from django.db import models -from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q, Prefetch +from django.db.models import Case, ExpressionWrapper, F, Prefetch, Q, Subquery, When from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from phonenumber_field.modelfields import PhoneNumberField -from timezone_field import TimeZoneField - from location.models import Address -from location.models import WineOriginAddressMixin from main.models import Award, Currency +from phonenumber_field.modelfields import PhoneNumberField from review.models import Review from tag.models import Tag from timetable.models import Timetable +from timezone_field import TimeZoneField from utils.methods import transform_into_readable_str -from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, - TranslatedFieldsMixin, BaseAttributes, GalleryMixin, - IntermediateGalleryModelMixin, HasTagsMixin, - FavoritesMixin, TypeDefaultImageMixin, FileMixin) +from utils.models import ( + BaseAttributes, FavoritesMixin, GalleryMixin, HasTagsMixin, IntermediateGalleryModelMixin, ProjectBaseMixin, + TJSONField, TranslatedFieldsMixin, TypeDefaultImageMixin, URLImageMixin, +) # todo: establishment type&subtypes check @@ -528,8 +526,8 @@ class EstablishmentQuerySet(models.QuerySet): filters.update({ 'id__in': models.Subquery( UserRole.objects.filter(user=user, role__site__country__code=country_code) - .distinct('user', 'establishment') - .values_list('establishment', flat=True) + .distinct('user', 'establishment') + .values_list('establishment', flat=True) ) }) return self.filter(**filters) @@ -1266,24 +1264,6 @@ class Plate(TranslatedFieldsMixin, models.Model): verbose_name_plural = _('plates') -class MenuDish(BaseAttributes): - """Dish model.""" - - STR_FIELD_NAME = 'category' - - name = models.CharField(_('name'), max_length=255, default='') - category = TJSONField( - blank=True, null=True, default=None, verbose_name=_('category'), - help_text='{"en-GB":"some text"}') - price = models.IntegerField(blank=True, null=True, default=None, verbose_name=_('price')) - signature = models.BooleanField(_('signature'), default=False) - - class Meta: - verbose_name = _('dish') - verbose_name_plural = _('dishes') - ordering = ('-created',) - - class MenuQuerySet(models.QuerySet): def with_schedule_plates_establishment(self): return self.select_related( @@ -1309,57 +1289,25 @@ class MenuQuerySet(models.QuerySet): """Search by category.""" return self.filter(category__icontains=value) - def with_dishes(self): - return self.filter(~Q(dishes=None)) - -class Menu(TranslatedFieldsMixin, BaseAttributes): +class Menu(GalleryMixin, TranslatedFieldsMixin, BaseAttributes): """Menu model.""" - name = models.CharField(_('name'), max_length=255, default='') + STR_FIELD_NAME = 'category' + + category = TJSONField( + blank=True, null=True, default=None, verbose_name=_('category'), + help_text='{"en-GB":"some text"}') establishment = models.ForeignKey( 'establishment.Establishment', verbose_name=_('establishment'), on_delete=models.CASCADE) is_drinks_included = models.BooleanField(_('is drinks included'), default=False) - price = models.IntegerField(blank=True, null=True, default=None, verbose_name=_('price')) schedule = models.ManyToManyField( to='timetable.Timetable', blank=True, - verbose_name=_('Menu schedule'), + verbose_name=_('Establishment schedule'), + related_name='menus', ) - uploads = models.ManyToManyField( - to='MenuFiles', - blank=True, - verbose_name=_('Menu files'), - ) - dishes = models.ManyToManyField( - to='MenuDish', - blank=True, - verbose_name=_('Menu dishes') - ) - category = TJSONField( - blank=True, null=True, default=None, verbose_name=_('category'), - help_text='{"en-GB":"some text"}') - - # a la cartes - average_starter_price = models.FloatField(default=0, blank=True, - verbose_name=_('average starter price')) - average_main_dish_price = models.FloatField(default=0, blank=True, - verbose_name=_('average main dish price')) - renewal_per_year = models.IntegerField(null=True, blank=True, - verbose_name=_('renewal per year')) - average_desert_price = models.FloatField(default=0, blank=True, - verbose_name=_('average desert price')) - nb_wine = models.IntegerField(null=True, blank=True, verbose_name=_('NB wine')) - lowest_price = models.FloatField(null=True, blank=True, verbose_name=_('dish lowest price')) - highest_price = models.FloatField(null=True, blank=True, verbose_name=_('dish highest price')) - served_by_glasses = models.NullBooleanField(null=True, - verbose_name=_('is wine served by glass')) - price_min_by_glass = models.FloatField(null=True, blank=True, - verbose_name=_('min price for wine served by glass')) - price_max_by_glass = models.FloatField(null=True, blank=True, - verbose_name=_('max price for wine served by glass')) - old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) objects = MenuQuerySet.as_manager() @@ -1370,15 +1318,43 @@ class Menu(TranslatedFieldsMixin, BaseAttributes): ordering = ('-created',) -class MenuFiles(FileMixin, BaseAttributes): - """Menu files""" - TYPES = ( - ('image', 'Image'), - ('file', 'File') +class MenuGallery(IntermediateGalleryModelMixin): + menu = models.ForeignKey( + Menu, + null=True, + related_name='menu_gallery', + on_delete=models.CASCADE, + verbose_name=_('menu'), + ) + image = models.ForeignKey( + 'gallery.Image', + null=True, + related_name='menu_gallery', + on_delete=models.CASCADE, + verbose_name=_('image'), ) - name = models.CharField(_('name'), max_length=255, default='') - type = models.CharField(_('type'), choices=TYPES, max_length=65, default='') + class Meta: + """Meta class.""" + verbose_name = _('menu gallery') + verbose_name_plural = _('menu galleries') + unique_together = (('menu', 'is_main'), ('menu', 'image')) + + +class MenuUploads(BaseAttributes): + """Menu files""" + + menu = models.ForeignKey( + Menu, + verbose_name=_('menu'), + on_delete=models.CASCADE, + related_name='menu_uploads', + ) + title = models.CharField(_('title'), max_length=255, default='') + file = models.FileField( + _('File'), + validators=[FileExtensionValidator(allowed_extensions=('doc', 'docx', 'pdf')), ], + ) class Meta: verbose_name = _('menu upload') diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 2b714b9b..668b4181 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -1,18 +1,15 @@ from functools import lru_cache -from django.db.models import F -from django.shortcuts import get_object_or_404 -from django.utils.translation import gettext_lazy as _ -from rest_framework import serializers - from account.serializers.common import UserShortSerializer +from django.db.models import F +from django.utils.translation import gettext_lazy as _ from establishment import models, serializers as model_serializers from establishment.models import ContactEmail, ContactPhone, EstablishmentEmployee -from establishment.serializers import MenuDishSerializer from gallery.models import Image from location.serializers import AddressDetailSerializer, TranslatedField from main.models import Currency from main.serializers import AwardSerializer +from rest_framework import serializers from utils.decorators import with_base_attributes from utils.serializers import ImageBaseSerializer, ProjectModelSerializer, TimeZoneChoiceField @@ -593,59 +590,109 @@ class EstablishmentEmployeePositionsSerializer(serializers.ModelSerializer): ] +class _PlateSerializer(ProjectModelSerializer): + name_translated = TranslatedField() + + class Meta: + model = models.Plate + fields = [ + 'name_translated', + 'price', + ] + + +class _MenuUploadsSerializer(serializers.Serializer): + id = serializers.IntegerField() + title = serializers.CharField() + original_url = serializers.URLField() + + class MenuDishesSerializer(ProjectModelSerializer): """for dessert, main_course and starter category""" - establishment_id = serializers.PrimaryKeyRelatedField(read_only=True) - establishment_slug = serializers.CharField(read_only=True, source='establishment.slug') - dishes = MenuDishSerializer(many=True, read_only=True) + plates = _PlateSerializer(read_only=True, many=True, source='plate_set', allow_null=True) + category_translated = serializers.CharField(read_only=True) + last_update = serializers.DateTimeField(source='created') + gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True) class Meta: model = models.Menu fields = [ 'id', - 'establishment_id', - 'establishment_slug', - 'dishes', - ] - - -class MenuDishesCreateSerializer(ProjectModelSerializer): - """Menu dishes create serializer""" - - menu_id = serializers.IntegerField(write_only=True) - - class Meta: - model = models.MenuDish - fields = [ - 'id', - 'name', 'category', - 'price', - 'signature', - 'menu_id' + 'category_translated', + 'establishment', + 'is_drinks_included', + 'schedule', + 'plates', + 'last_update', + 'gallery', ] - def create(self, validated_data): - menu_id = validated_data.pop('menu_id') - menu = get_object_or_404(models.Menu, pk=menu_id) - instance = models.MenuDish.objects.create(**validated_data) - menu.dishes.add(instance) - return instance - class MenuDishesRUDSerializers(ProjectModelSerializer): """for dessert, main_course and starter category""" - establishment_id = serializers.PrimaryKeyRelatedField(queryset=models.Establishment.objects.all()) - establishment_slug = serializers.CharField(read_only=True, source='establishment.slug') - dishes = MenuDishSerializer(many=True, read_only=True) + plates = _PlateSerializer(read_only=True, many=True, source='plate_set', allow_null=True) + gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True) class Meta: model = models.Menu fields = [ 'id', - 'establishment_id', - 'establishment_slug', - 'dishes', + 'category', + 'plates', + 'establishment', + 'is_drinks_included', + 'schedule', + 'gallery', ] + + +class MenuGallerySerializer(serializers.ModelSerializer): + """Serializer class for model MenuGallery.""" + + class Meta: + """Meta class""" + + model = models.MenuGallery + fields = [ + 'id', + 'is_main', + ] + + @property + def request_kwargs(self): + """Get url kwargs from request.""" + return self.context.get('request').parser_context.get('kwargs') + + def create(self, validated_data): + menu_pk = self.request_kwargs.get('pk') + image_id = self.request_kwargs.get('image_id') + qs = models.MenuGallery.objects.filter(image_id=image_id, menu_id=menu_pk) + instance = qs.first() + if instance: + qs.update(**validated_data) + return instance + return super().create(validated_data) + + def validate(self, attrs): + """Override validate method.""" + menu_pk = self.request_kwargs.get('pk') + image_id = self.request_kwargs.get('image_id') + + menu_qs = models.Menu.objects.filter(pk=menu_pk) + image_qs = Image.objects.filter(id=image_id) + + if not menu_qs.exists(): + raise serializers.ValidationError({'detail': _('Menu not found')}) + if not image_qs.exists(): + raise serializers.ValidationError({'detail': _('Image not found')}) + + menu = menu_qs.first() + image = image_qs.first() + + attrs['menu'] = menu + attrs['image'] = image + + return attrs diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index 09c2f2a8..d5173942 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -30,15 +30,13 @@ urlpatterns = [ name='note-rud'), path('slug//admin/', views.EstablishmentAdminView.as_view(), name='establishment-admin-list'), - path('menus/dishes/', views.MenuDishesListView.as_view(), name='menu-dishes-list'), - path('menus/dishes/create/', views.MenuDishesCreateView.as_view(), name='menu-dishes-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/slug//', views.MenuDishesRUDView.as_view(), name='menu-dishes-rud'), + path('menus/dishes//gallery/', views.MenuGalleryListView.as_view(), name='menu-dishes-gallery-list'), + path('menus/dishes//gallery//', views.MenuGalleryCreateDestroyView.as_view(), + name='menu-dishes-gallery-create-destroy'), path('menus/', views.MenuListCreateView.as_view(), name='menu-list'), path('menus//', views.MenuRUDView.as_view(), name='menu-rud'), - path('menus/slug//', views.MenuRUDView.as_view(), name='menu-rud'), - path('menus/uploads/', views.MenuFilesListCreateView.as_view(), name='menu-files-list'), - path('menus/uploads//', views.MenuFilesRUDView.as_view(), name='menu-files-rud'), path('plates/', views.PlateListCreateView.as_view(), name='plates'), path('plates//', views.PlateRUDView.as_view(), name='plate-rud'), path('social_choice/', views.SocialChoiceListCreateView.as_view(), name='socials_choice'), diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index beabf801..d54f3702 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -12,8 +12,7 @@ from establishment.models import EstablishmentEmployee from timetable.models import Timetable from timetable.serialziers import ScheduleCreateSerializer, ScheduleRUDSerializer from utils.methods import get_permission_classes -from utils.permissions import ( - IsEstablishmentManager, IsEstablishmentAdministrator, ) +from utils.permissions import (IsEstablishmentAdministrator, IsEstablishmentManager) from utils.views import CreateDestroyGalleryViewMixin @@ -160,23 +159,19 @@ class MenuListCreateView(generics.ListCreateAPIView): """Menu list create view.""" serializer_class = serializers.MenuSerializers queryset = models.Menu.objects.all() - filter_backends = (DjangoFilterBackend,) permission_classes = get_permission_classes( IsEstablishmentManager, IsEstablishmentAdministrator, ) + filter_backends = (DjangoFilterBackend,) filterset_fields = ( - 'establishment__id', + 'establishment', 'establishment__slug', ) - def get_queryset(self): - return super().get_queryset().prefetch_related('establishment') - -class MenuRUDView(MenuRUDMixinViews, generics.RetrieveUpdateDestroyAPIView): +class MenuRUDView(generics.RetrieveUpdateDestroyAPIView): """Menu RUD view.""" - lookup_field = None serializer_class = serializers.MenuRUDSerializers queryset = models.Menu.objects.all() permission_classes = get_permission_classes( @@ -185,26 +180,6 @@ class MenuRUDView(MenuRUDMixinViews, generics.RetrieveUpdateDestroyAPIView): ) -class MenuFilesListCreateView(generics.ListCreateAPIView): - """Menu files list create view.""" - serializer_class = serializers.MenuFilesSerializers - queryset = models.MenuFiles.objects.all() - permission_classes = get_permission_classes( - IsEstablishmentManager, - IsEstablishmentAdministrator - ) - - -class MenuFilesRUDView(generics.RetrieveDestroyAPIView): - """Menu files RUD view.""" - serializer_class = serializers.MenuFilesSerializers - queryset = models.MenuFiles.objects.all() - permission_classes = get_permission_classes( - IsEstablishmentManager, - IsEstablishmentAdministrator - ) - - class SocialChoiceListCreateView(generics.ListCreateAPIView): """SocialChoice list create view.""" serializer_class = serializers.SocialChoiceSerializers @@ -614,6 +589,7 @@ class MenuDishesRUDView(MenuRUDMixinViews, generics.RetrieveUpdateDestroyAPIView IsEstablishmentAdministrator ) + class MenuDishesCreateView(generics.CreateAPIView): """Menu (dessert, main_course, starter) list create view.""" serializer_class = serializers.MenuDishesCreateSerializer