diff --git a/apps/establishment/migrations/0084_auto_20200129_1113.py b/apps/establishment/migrations/0084_auto_20200129_1113.py new file mode 100644 index 00000000..c041c7bd --- /dev/null +++ b/apps/establishment/migrations/0084_auto_20200129_1113.py @@ -0,0 +1,69 @@ +# Generated by Django 2.2.7 on 2020-01-29 11:13 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import utils.methods + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('establishment', '0083_establishment_instagram'), + ] + + operations = [ + migrations.CreateModel( + name='MenuFiles', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('file', models.FileField(blank=True, default=None, null=True, upload_to=utils.methods.file_path, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=('jpg', 'jpeg', 'png', 'doc', 'docx', 'pdf'))], verbose_name='File')), + ('name', models.CharField(default='', max_length=255, verbose_name='name')), + ('type', models.CharField(default='', max_length=65, verbose_name='type')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='menufiles_records_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')), + ('modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='menufiles_records_modified', to=settings.AUTH_USER_MODEL, verbose_name='modified by')), + ], + options={ + 'verbose_name': 'menu upload', + 'verbose_name_plural': 'menu uploads', + }, + ), + migrations.RemoveField( + model_name='menuuploads', + name='created_by', + ), + migrations.RemoveField( + model_name='menuuploads', + name='menu', + ), + migrations.RemoveField( + model_name='menuuploads', + name='modified_by', + ), + migrations.AddField( + model_name='menu', + name='name', + field=models.CharField(default='', max_length=255, verbose_name='name'), + ), + migrations.AlterField( + model_name='menu', + name='schedule', + field=models.ManyToManyField(blank=True, to='timetable.Timetable', verbose_name='Menu schedule'), + ), + migrations.DeleteModel( + name='MenuGallery', + ), + migrations.DeleteModel( + name='MenuUploads', + ), + migrations.AddField( + model_name='menu', + name='uploads', + field=models.ManyToManyField(blank=True, to='establishment.MenuFiles', verbose_name='Menu files'), + ), + ] diff --git a/apps/establishment/migrations/0085_menu_price.py b/apps/establishment/migrations/0085_menu_price.py new file mode 100644 index 00000000..23afcba4 --- /dev/null +++ b/apps/establishment/migrations/0085_menu_price.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2020-01-29 11:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0084_auto_20200129_1113'), + ] + + operations = [ + migrations.AddField( + model_name='menu', + name='price', + field=models.IntegerField(blank=True, default=None, null=True, verbose_name='price'), + ), + ] diff --git a/apps/establishment/migrations/0086_auto_20200129_1301.py b/apps/establishment/migrations/0086_auto_20200129_1301.py new file mode 100644 index 00000000..99ad4d31 --- /dev/null +++ b/apps/establishment/migrations/0086_auto_20200129_1301.py @@ -0,0 +1,47 @@ +# Generated by Django 2.2.7 on 2020-01-29 13:01 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('establishment', '0085_menu_price'), + ] + + operations = [ + migrations.AlterField( + model_name='menufiles', + name='type', + field=models.CharField(choices=[('image', 'Image'), ('file', 'File')], default='', max_length=65, verbose_name='type'), + ), + migrations.CreateModel( + name='MenuDish', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('name', models.CharField(default='', max_length=255, verbose_name='name')), + ('category', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='category')), + ('price', models.IntegerField(blank=True, default=None, null=True, verbose_name='price')), + ('signature', models.BooleanField(default=False, verbose_name='signature')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='menudish_records_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')), + ('modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='menudish_records_modified', to=settings.AUTH_USER_MODEL, verbose_name='modified by')), + ], + options={ + 'verbose_name': 'dish', + 'verbose_name_plural': 'dishes', + 'ordering': ('-created',), + }, + ), + migrations.AddField( + model_name='menu', + name='dishes', + field=models.ManyToManyField(blank=True, to='establishment.MenuDish', verbose_name='Menu dishes'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index b1adf435..9999d25a 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -6,34 +6,33 @@ from typing import List import elasticsearch_dsl from django.conf import settings -from django.shortcuts import get_object_or_404 from django.contrib.contenttypes import fields as generic from django.contrib.gis.db.models.functions import Distance from django.contrib.gis.geos import Point from django.contrib.gis.measure import Distance as DistanceMeasure from django.contrib.postgres.fields import ArrayField -from django.contrib.postgres.search import TrigramDistance, TrigramSimilarity 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, FileExtensionValidator +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q, Prefetch, Sum +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 timetable.models import Timetable -from location.models import WineOriginAddressMixin from main.models import Award, Currency from review.models import Review from tag.models import Tag +from timetable.models import Timetable from utils.methods import transform_into_readable_str -from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, - TranslatedFieldsMixin, BaseAttributes, GalleryMixin, - IntermediateGalleryModelMixin, HasTagsMixin, - FavoritesMixin, TypeDefaultImageMixin) +from utils.models import ( + BaseAttributes, FavoritesMixin, FileMixin, GalleryMixin, HasTagsMixin, IntermediateGalleryModelMixin, + ProjectBaseMixin, TJSONField, TranslatedFieldsMixin, TypeDefaultImageMixin, URLImageMixin, +) # todo: establishment type&subtypes check @@ -561,8 +560,8 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, verbose_name=_('Facebook URL')) twitter = models.URLField(blank=True, null=True, default=None, max_length=255, verbose_name=_('Twitter URL')) - instagram =models.URLField(blank=True, null=True, default=None, max_length=255, - verbose_name=_('Instagram URL')) + instagram = models.URLField(blank=True, null=True, default=None, max_length=255, + verbose_name=_('Instagram URL')) lafourchette = models.URLField(blank=True, null=True, default=None, max_length=255, verbose_name=_('Lafourchette URL')) guestonline_id = models.PositiveIntegerField(blank=True, verbose_name=_('guestonline id'), @@ -734,7 +733,8 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, now_at_est_tz = datetime.now(tz=self.tz) current_week = now_at_est_tz.weekday() schedule_for_today = self.schedule.filter(weekday=current_week).first() - if schedule_for_today is None or schedule_for_today.opening_time is None or schedule_for_today.ending_time is None: + if schedule_for_today is None or schedule_for_today.opening_time is None or schedule_for_today.ending_time is \ + None: return False time_at_est_tz = now_at_est_tz.time() return schedule_for_today.ending_time > time_at_est_tz > schedule_for_today.opening_time @@ -746,8 +746,10 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, @property def tags_indexing(self): - return [{'id': tag.metadata.id, - 'label': tag.metadata.label} for tag in self.tags.all()] + return [{ + 'id': tag.metadata.id, + 'label': tag.metadata.label + } for tag in self.tags.all()] @property def last_published_review(self): @@ -1241,6 +1243,24 @@ 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( @@ -1266,25 +1286,38 @@ 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(GalleryMixin, TranslatedFieldsMixin, BaseAttributes): + +class Menu(TranslatedFieldsMixin, BaseAttributes): """Menu model.""" - STR_FIELD_NAME = 'category' - - category = TJSONField( - blank=True, null=True, default=None, verbose_name=_('category'), - help_text='{"en-GB":"some text"}') + name = models.CharField(_('name'), max_length=255, default='') 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=_('Establishment schedule'), - related_name='menus', + verbose_name=_('Menu schedule'), ) + 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"}') + old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) objects = MenuQuerySet.as_manager() @@ -1295,43 +1328,15 @@ class Menu(GalleryMixin, TranslatedFieldsMixin, BaseAttributes): ordering = ('-created',) -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'), - ) - - class Meta: - """Meta class.""" - verbose_name = _('menu gallery') - verbose_name_plural = _('menu galleries') - unique_together = (('menu', 'is_main'), ('menu', 'image')) - - -class MenuUploads(BaseAttributes): +class MenuFiles(FileMixin, BaseAttributes): """Menu files""" + TYPES = ( + ('image', 'Image'), + ('file', 'File') + ) - 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')), ], - ) + name = models.CharField(_('name'), max_length=255, default='') + type = models.CharField(_('type'), choices=TYPES, max_length=65, default='') class Meta: verbose_name = _('menu upload') diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 634d10b4..b184e033 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -2,12 +2,14 @@ from functools import lru_cache from pprint import pprint 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 establishment import models, serializers as model_serializers -from establishment.models import ContactPhone, EstablishmentEmployee, ContactEmail +from establishment.models import ContactEmail, ContactPhone, EstablishmentEmployee +from establishment.serializers import MenuDishSerializer from gallery.models import Image from location.models import Address from location.serializers import AddressDetailSerializer, TranslatedField @@ -584,109 +586,59 @@ 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""" - 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) + establishment_id = serializers.PrimaryKeyRelatedField(read_only=True) + establishment_slug = serializers.CharField(read_only=True, source='establishment.slug') + dishes = MenuDishSerializer(many=True, read_only=True) class Meta: model = models.Menu fields = [ 'id', - 'category', - 'category_translated', - 'establishment', - 'is_drinks_included', - 'schedule', - 'plates', - 'last_update', - 'gallery', + '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' + ] + + 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""" - plates = _PlateSerializer(read_only=True, many=True, source='plate_set', allow_null=True) - gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True) + 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) class Meta: model = models.Menu fields = [ 'id', - 'category', - 'plates', - 'establishment', - 'is_drinks_included', - 'schedule', - 'gallery', + 'establishment_id', + 'establishment_slug', + 'dishes', ] - - -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/serializers/common.py b/apps/establishment/serializers/common.py index 5d53eec8..3af43e27 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -2,6 +2,7 @@ import logging from django.conf import settings +from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from phonenumber_field.phonenumber import to_python as str_to_phonenumber from rest_framework import serializers @@ -9,18 +10,19 @@ from rest_framework import serializers from comment import models as comment_models from comment.serializers import common as comment_serializers from establishment import models -from location.serializers import AddressBaseSerializer, CityBaseSerializer, AddressDetailSerializer, \ - CityShortSerializer -from location.serializers import EstablishmentWineRegionBaseSerializer, \ - EstablishmentWineOriginBaseSerializer +from location.serializers import ( + AddressBaseSerializer, AddressDetailSerializer, CityBaseSerializer, + CityShortSerializer, EstablishmentWineOriginBaseSerializer, EstablishmentWineRegionBaseSerializer, +) from main.serializers import AwardSerializer, CurrencySerializer from review.serializers import ReviewShortSerializer from tag.serializers import TagBaseSerializer from timetable.serialziers import ScheduleRUDSerializer from utils import exceptions as utils_exceptions -from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer -from utils.serializers import (ProjectModelSerializer, TranslatedField, - FavoritesCreateSerializer) +from utils.serializers import ( + CarouselCreateSerializer, FavoritesCreateSerializer, ImageBaseSerializer, + ProjectModelSerializer, TranslatedField, +) logger = logging.getLogger(__name__) @@ -70,31 +72,87 @@ class PlateSerializer(ProjectModelSerializer): ] -class MenuSerializers(ProjectModelSerializer): - plates = PlateSerializer(read_only=True, many=True, source='plate_set') - category_translated = serializers.CharField(read_only=True) - +class MenuDishSerializer(ProjectModelSerializer): class Meta: - model = models.Menu + model = models.MenuDish fields = [ 'id', + 'name', 'category', - 'category_translated', - 'plates', - 'establishment' + 'price', + 'signature' ] -class MenuRUDSerializers(ProjectModelSerializer): - plates = PlateSerializer(read_only=True, many=True, source='plate_set') +class MenuFilesSerializers(ProjectModelSerializer): + menu_id = serializers.IntegerField(write_only=True) + + class Meta: + model = models.MenuFiles + fields = [ + 'id', + 'name', + 'type', + 'file', + 'menu_id' + ] + + def create(self, validated_data): + menu_id = validated_data.pop('menu_id') + menu = get_object_or_404(models.Menu, pk=menu_id) + instance = models.MenuFiles.objects.create(**validated_data) + menu.uploads.add(instance) + return instance + + +class MenuSerializers(ProjectModelSerializer): + name = serializers.CharField() + establishment_id = serializers.PrimaryKeyRelatedField(queryset=models.Establishment.objects.all()) + establishment_slug = serializers.CharField(read_only=True, source='establishment.slug') + price = serializers.IntegerField(required=False) + drinks_included = serializers.BooleanField(source='is_drinks_included', required=False) + schedules = ScheduleRUDSerializer(many=True, allow_null=True, required=False) + uploads = MenuFilesSerializers(many=True, read_only=True) class Meta: model = models.Menu fields = [ 'id', - 'category', - 'plates', - 'establishment' + 'name', + 'establishment_id', + 'establishment_slug', + 'price', + 'drinks_included', + 'schedules', + 'uploads', + ] + + def create(self, validated_data): + validated_data['establishment'] = validated_data.pop('establishment_id') + instance = models.Menu.objects.create(**validated_data) + return instance + + +class MenuRUDSerializers(ProjectModelSerializer): + name = serializers.CharField() + establishment_id = serializers.PrimaryKeyRelatedField(read_only=True) + establishment_slug = serializers.CharField(read_only=True, source='establishment.slug') + price = serializers.IntegerField(required=False) + drinks_included = serializers.BooleanField(source='is_drinks_included', required=False) + schedules = ScheduleRUDSerializer(many=True, allow_null=True, required=False) + uploads = MenuFilesSerializers(many=True) + + class Meta: + model = models.Menu + fields = [ + 'id', + 'name', + 'establishment_id', + 'establishment_slug', + 'price', + 'drinks_included', + 'schedules', + 'uploads', ] @@ -528,7 +586,7 @@ class EstablishmentCommentBaseSerializer(comment_serializers.CommentBaseSerializ 'created', 'text', 'mark', - 'nickname', + # 'nickname', 'profile_pic', 'status', 'status_display', @@ -568,7 +626,7 @@ class EstablishmentCommentRUDSerializer(comment_serializers.CommentBaseSerialize 'created', 'text', 'mark', - 'nickname', + # 'nickname', 'profile_pic', ] diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index d5173942..09c2f2a8 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -30,13 +30,15 @@ urlpatterns = [ name='note-rud'), path('slug//admin/', views.EstablishmentAdminView.as_view(), name='establishment-admin-list'), - path('menus/dishes/', views.MenuDishesListCreateView.as_view(), name='menu-dishes-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.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/dishes/slug//', views.MenuDishesRUDView.as_view(), name='menu-dishes-rud'), 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 d745f5e9..39ede5c3 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -1,8 +1,9 @@ """Establishment app views.""" - +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 -from rest_framework import generics, permissions, response +from rest_framework import generics, permissions from rest_framework.response import Response from account.models import User @@ -14,6 +15,20 @@ from utils.permissions import IsCountryAdmin, IsEstablishmentManager, IsWineryRe from utils.views import CreateDestroyGalleryViewMixin +class MenuRUDMixinViews: + """Menu mixin""" + + def get_object(self): + instance = self.get_queryset().filter( + Q(establishment__slug=self.kwargs.get('slug')) | Q(establishment__id=self.kwargs.get('pk')) + ).first() + + if instance is None: + raise Http404 + + return instance + + class EstablishmentMixinViews: """Establishment mixin.""" @@ -121,18 +136,37 @@ class MenuListCreateView(generics.ListCreateAPIView): permission_classes = [IsWineryReviewer | IsEstablishmentManager] filter_backends = (DjangoFilterBackend,) filterset_fields = ( - 'establishment', + 'establishment__id', 'establishment__slug', ) + def get_queryset(self): + return super().get_queryset().prefetch_related('establishment') -class MenuRUDView(generics.RetrieveUpdateDestroyAPIView): + +class MenuRUDView(MenuRUDMixinViews, generics.RetrieveUpdateDestroyAPIView): """Menu RUD view.""" + + lookup_field = None serializer_class = serializers.MenuRUDSerializers queryset = models.Menu.objects.all() permission_classes = [IsWineryReviewer | IsEstablishmentManager] +class MenuFilesListCreateView(generics.ListCreateAPIView): + """Menu files list create view.""" + serializer_class = serializers.MenuFilesSerializers + queryset = models.MenuFiles.objects.all() + permission_classes = [IsWineryReviewer | IsEstablishmentManager] + + +class MenuFilesRUDView(generics.RetrieveDestroyAPIView): + """Menu files RUD view.""" + serializer_class = serializers.MenuFilesSerializers + queryset = models.MenuFiles.objects.all() + permission_classes = [IsWineryReviewer | IsEstablishmentManager] + + class SocialChoiceListCreateView(generics.ListCreateAPIView): """SocialChoice list create view.""" serializer_class = serializers.SocialChoiceSerializers @@ -449,69 +483,26 @@ class EstablishmentAdminView(generics.ListAPIView): return User.objects.establishment_admin(establishment).distinct() -class MenuDishesListCreateView(generics.ListCreateAPIView): +class MenuDishesListView(generics.ListAPIView): """Menu (dessert, main_course, starter) list create view.""" serializer_class = serializers.MenuDishesSerializer - queryset = models.Menu.objects.with_schedule_plates_establishment().dishes().distinct() + queryset = models.Menu.objects.with_dishes() permission_classes = [IsWineryReviewer | IsEstablishmentManager] filter_class = filters.MenuDishesBackFilter -class MenuDishesRUDView(generics.RetrieveUpdateDestroyAPIView): +class MenuDishesRUDView(MenuRUDMixinViews, generics.RetrieveUpdateDestroyAPIView): """Menu (dessert, main_course, starter) RUD view.""" + + lookup_field = None serializer_class = serializers.MenuDishesRUDSerializers - queryset = models.Menu.objects.dishes().distinct() + queryset = models.Menu.objects.with_dishes() permission_classes = [IsWineryReviewer | IsEstablishmentManager] -class MenuGalleryListView(generics.ListAPIView): - """Resource for returning gallery for menu for back-office users.""" - serializer_class = serializers.ImageBaseSerializer +class MenuDishesCreateView(generics.CreateAPIView): + """Menu (dessert, main_course, starter) list create view.""" + serializer_class = serializers.MenuDishesCreateSerializer + queryset = models.MenuDish.objects.all() permission_classes = [IsWineryReviewer | IsEstablishmentManager] - queryset = models.Menu.objects.with_schedule_plates_establishment().with_gallery().dishes() - - def get_object(self): - """Override get_object method.""" - qs = super(MenuGalleryListView, self).get_queryset() - menu = get_object_or_404(qs, pk=self.kwargs.get('pk')) - - # May raise a permission denied - # self.check_object_permissions(self.request, menu) - - return menu - - def get_queryset(self): - """Override get_queryset method.""" - return self.get_object().crop_gallery - - -class MenuGalleryCreateDestroyView(CreateDestroyGalleryViewMixin): - """Resource for a create gallery for menu for back-office users.""" - serializer_class = serializers.MenuGallerySerializer - permission_classes = [IsWineryReviewer | IsEstablishmentManager] - - def get_queryset(self): - """Override get_queryset method.""" - qs = models.Menu.objects.with_schedule_plates_establishment().with_gallery().dishes() - return qs - - def create(self, request, *args, **kwargs): - _ = super().create(request, *args, **kwargs) - news_qs = self.filter_queryset(self.get_queryset()) - return response.Response( - data=serializers.MenuDishesRUDSerializers(get_object_or_404(news_qs, pk=kwargs.get('pk'))).data - ) - - def get_object(self): - """ - Returns the object the view is displaying. - """ - menu_qs = self.filter_queryset(self.get_queryset()) - - menu = get_object_or_404(menu_qs, pk=self.kwargs.get('pk')) - gallery = get_object_or_404(menu.menu_gallery, image_id=self.kwargs.get('image_id')) - - # May raise a permission denied - self.check_object_permissions(self.request, gallery) - - return gallery + filter_class = filters.MenuDishesBackFilter diff --git a/apps/utils/methods.py b/apps/utils/methods.py index 58cc3f52..839105e2 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -6,7 +6,7 @@ import string from collections import namedtuple from functools import reduce from io import BytesIO - +import pathlib import requests from django.conf import settings from django.contrib.contenttypes.models import ContentType @@ -69,6 +69,15 @@ def username_random(): ) +def file_path(instance, filename): + """Determine file path method.""" + filename = '%s.%s' % (generate_code(), pathlib.Path(filename).suffix.lstrip('.')) + return 'files/%s/%s/%s' % ( + instance._meta.model_name, + datetime.now().strftime(settings.REST_DATE_FORMAT), + filename) + + def image_path(instance, filename): """Determine avatar path method.""" filename = '%s.jpeg' % generate_code() diff --git a/apps/utils/models.py b/apps/utils/models.py index d58a1bbc..281c57ca 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -8,6 +8,7 @@ from django.contrib.gis.db import models from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields.jsonb import KeyTextTransform +from django.core.validators import FileExtensionValidator from django.utils import timezone from django.utils.html import mark_safe from django.utils.translation import ugettext_lazy as _, get_language @@ -16,7 +17,7 @@ from sorl.thumbnail import get_thumbnail from sorl.thumbnail.fields import ImageField as SORLImageField from configuration.models import TranslationSettings -from utils.methods import image_path, svg_image_path +from utils.methods import image_path, svg_image_path, file_path from utils.validators import svg_image_validator logger = logging.getLogger(__name__) @@ -86,6 +87,7 @@ def translate_field(self, field_name, toggle_field_name=None): return None return value return None + return translate @@ -159,6 +161,33 @@ class BaseAttributes(ProjectBaseMixin): abstract = True +class FileMixin(models.Model): + """File model.""" + + file = models.FileField(upload_to=file_path, + blank=True, null=True, default=None, + verbose_name=_('File'), + validators=[FileExtensionValidator( + allowed_extensions=('jpg', 'jpeg', 'png', 'doc', 'docx', 'pdf') + )]) + + class Meta: + """Meta class.""" + + abstract = True + + def get_file_url(self): + """Get file url.""" + return self.file.url if self.file else None + + def get_full_file_url(self, request): + """Get full file url""" + if self.file and exists(self.file.path): + return request.build_absolute_uri(self.file.url) + else: + return None + + class ImageMixin(models.Model): """Avatar model.""" @@ -230,7 +259,7 @@ class SORLImageMixin(models.Model): else: return None - def get_cropped_image(self, geometry: str, quality: int, cropbox:str) -> dict: + def get_cropped_image(self, geometry: str, quality: int, cropbox: str) -> dict: cropped_image = get_thumbnail(self.image, geometry_string=geometry, # crop=crop,