From b6cf12f35c177c91daf1c3dcb8c72b6975f5d4a1 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Fri, 7 Feb 2020 08:27:54 +0000 Subject: [PATCH 01/36] save address cascade for establishment --- apps/establishment/serializers/back.py | 10 +++------- apps/location/serializers/common.py | 9 ++++++++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index a24123b0..9ac77045 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -167,13 +167,9 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria validated_data['slug'] = slug if 'address' in validated_data: - address_fields = validated_data.pop('address') - address_instance = get_object_or_404(models.Address, id=address_fields['id'] or None) - address_id = getattr(address_instance, 'id') - - models.Address.objects.filter(id=address_id).update(**address_fields) - - validated_data['address_id'] = address_id + address = models.Address(**validated_data.pop('address')) + address.save() + validated_data['address_id'] = address.id instance = super().create(validated_data) diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index b4ea9c5f..e724979f 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -223,11 +223,17 @@ class AddressBaseSerializer(serializers.ModelSerializer): class AddressEstablishmentSerializer(AddressBaseSerializer): """Address serializer.""" - id = serializers.IntegerField(required=True) + id = serializers.IntegerField(required=False) street_name_1 = serializers.CharField(required=False, default='') street_name_2 = serializers.CharField(required=False, default='') number = serializers.IntegerField(required=False, default=0) postal_code = serializers.CharField(required=False, default='') + city_id = serializers.PrimaryKeyRelatedField( + source='city', + queryset=models.City.objects.all(), + write_only=True, + required=True, + ) class Meta(AddressBaseSerializer.Meta): """Meta class.""" @@ -238,6 +244,7 @@ class AddressEstablishmentSerializer(AddressBaseSerializer): 'street_name_2', 'number', 'postal_code', + 'city_id', ) From 40c730bdd3ccd9e860817e3c182b73d011be8fc2 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Fri, 7 Feb 2020 09:31:40 +0000 Subject: [PATCH 02/36] allow blank street_name --- apps/location/serializers/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index e724979f..8c9cf07b 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -224,8 +224,8 @@ class AddressEstablishmentSerializer(AddressBaseSerializer): """Address serializer.""" id = serializers.IntegerField(required=False) - street_name_1 = serializers.CharField(required=False, default='') - street_name_2 = serializers.CharField(required=False, default='') + street_name_1 = serializers.CharField(required=False, allow_blank=True, default='') + street_name_2 = serializers.CharField(required=False, allow_blank=True, default='') number = serializers.IntegerField(required=False, default=0) postal_code = serializers.CharField(required=False, default='') city_id = serializers.PrimaryKeyRelatedField( From 26b7ef8ad654b96bb80625636383f24d702f8431 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 7 Feb 2020 12:33:08 +0300 Subject: [PATCH 03/36] added a documentation for awards --- apps/main/serializers/common.py | 64 +++++++++++----- apps/main/urls/back.py | 3 +- apps/main/views/back.py | 125 +++++++++++++++++++++++++++++++- apps/utils/exceptions.py | 5 ++ 4 files changed, 175 insertions(+), 22 deletions(-) diff --git a/apps/main/serializers/common.py b/apps/main/serializers/common.py index 4f7ad68f..fd3e32fe 100644 --- a/apps/main/serializers/common.py +++ b/apps/main/serializers/common.py @@ -1,11 +1,14 @@ """Main app serializers.""" +from typing import Union + from django.contrib.contenttypes.models import ContentType from rest_framework import serializers +from establishment.models import Employee from location.serializers import CountrySerializer from main import models -from establishment.models import Employee from tag.serializers import TagBackOfficeSerializer +from utils.exceptions import EmployeeNotFoundError from utils.serializers import ProjectModelSerializer, RecursiveFieldSerializer, TranslatedField @@ -207,6 +210,7 @@ class AwardBaseSerializer(serializers.ModelSerializer): """Award base serializer.""" title_translated = serializers.CharField(read_only=True, allow_null=True) + title = serializers.CharField(write_only=True, help_text='Title text') class Meta: model = models.Award @@ -215,45 +219,63 @@ class AwardBaseSerializer(serializers.ModelSerializer): 'title_translated', 'vintage_year', 'image_url', + 'title', ] + @property + def request(self): + """Return a request object""" + return self.context.get('request') + + @property + def context_kwargs(self) -> Union[dict, None]: + """Return a request kwargs.""" + if hasattr(self.request, 'parser_context'): + return self.request.parser_context.get('kwargs') + + def validate_title(self, value) -> dict: + """Construct title str to JSON that contains locale from request.""" + return {self.request.locale: value} + class AwardSerializer(AwardBaseSerializer): """Award serializer.""" - award_type = AwardTypeBaseSerializer(read_only=True) - class Meta: - model = models.Award + class Meta(AwardBaseSerializer.Meta): fields = AwardBaseSerializer.Meta.fields + ['award_type', ] class BackAwardSerializer(AwardBaseSerializer): """Award serializer.""" + award_type_display = AwardTypeBaseSerializer(read_only=True, + source='award_type') + award_type = serializers.PrimaryKeyRelatedField( + queryset=models.AwardType.objects.all(), + write_only=True, + required=True, + ) - award_type = AwardTypeBaseSerializer(read_only=True) - - class Meta: - model = models.Award + class Meta(AwardBaseSerializer.Meta): fields = AwardBaseSerializer.Meta.fields + [ 'award_type', + 'award_type_display', 'state', 'content_type', 'object_id', ] + def to_representation(self, instance): + data = super(BackAwardSerializer, self).to_representation(instance) + data['award_type'] = data.pop('award_type_display', None) + return data -class BackAwardEmployeeCreateSerializer(serializers.ModelSerializer): + +class BackAwardEmployeeCreateSerializer(AwardBaseSerializer): """Award, The Creator.""" - award_type = serializers.PrimaryKeyRelatedField(required=True, queryset=models.AwardType.objects.all()) - title = serializers.CharField(write_only=True) - def get_title(self, obj): - pass - - class Meta: - model = models.Award + class Meta(AwardBaseSerializer.Meta): fields = ( 'id', 'award_type', @@ -262,9 +284,15 @@ class BackAwardEmployeeCreateSerializer(serializers.ModelSerializer): ) def validate(self, attrs): - attrs['object_id'] = self.context.get('request').parser_context.get('kwargs')['employee_id'] + """An overridden validate method.""" + employee_id = self.context_kwargs.get('employee_id') + employee_qs = Employee.objects.filter(id=employee_id) + + if not employee_qs.exists(): + raise EmployeeNotFoundError() + + attrs['object_id'] = employee_id attrs['content_type'] = ContentType.objects.get_for_model(Employee) - attrs['title'] = {self.context.get('request').locale: attrs['title']} return attrs diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py index 8b8f5102..8b1cd6e4 100644 --- a/apps/main/urls/back.py +++ b/apps/main/urls/back.py @@ -8,7 +8,8 @@ app_name = 'main' urlpatterns = [ path('awards/', views.AwardLstView.as_view(), name='awards-list-create'), path('awards//', views.AwardRUDView.as_view(), name='awards-rud'), - path('awards/create-and-bind//', views.AwardCreateAndBind.as_view(), name='award-employee-create'), + path('awards/create-and-bind//', views.AwardCreateAndBind.as_view(), + name='award-employee-create'), path('award-types/', views.AwardTypesListView.as_view(), name='awards-types-list'), path('content_type/', views.ContentTypeView.as_view(), name='content_type-list'), path('sites/', views.SiteListBackOfficeView.as_view(), name='site-list-create'), diff --git a/apps/main/views/back.py b/apps/main/views/back.py index 5584cf00..8f264ac6 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -17,7 +17,53 @@ from utils.methods import get_permission_classes class AwardLstView(generics.ListCreateAPIView): - """Award list create view.""" + """ + ## List of awards + ### *GET* + #### Description + Return paginated list of awards. + Available filters: + * establishment_id (`int`) - Filter by establishment identifier + * product_id (`int`) - Filter by product identifier + * employee_id (`int`) - Filter by employee identifier + * state (`enum`) - `0 (Waiting)`, `1 (Published)` + * award_type (`str`) - Filter by award type identifier + * vintage_year (`str`) - Filter by a vintage year + ##### Response + E.g.: + ``` + { + "count": 58, + "next": 2, + "previous": null, + "results": [ + { + "id": 1, + ... + } + ] + } + ``` + + ### *POST* + #### Description + Create a record in Award table. + ##### Request + Required: + * content_type (`int`) - identifier of content type entity + * object_id (`int`) - identifier of content object + * award_type (`int`) - identifier of award type + * title (`str`) - title of an award + Non required: + * vintage_year (str) - vintage year in a format - `yyyy` + ##### Response + ``` + { + "id": 1, + ... + } + ``` + """ queryset = Award.objects.all().with_base_related() serializer_class = serializers.BackAwardSerializer permission_classes = get_permission_classes() @@ -25,7 +71,35 @@ class AwardLstView(generics.ListCreateAPIView): class AwardCreateAndBind(generics.CreateAPIView): - """Award create and bind to employee by id""" + """ + ## Creating an Award for an Employee. + ### *POST* + #### Description + Creating an Award for an Employee and return in response + serialized Employee object. + ##### Response + E.g. + ``` + { + "id": 1, + ... + } + ``` + ##### Request + Required: + * award_type (`int`) - identifier of award type + * title (`str`) - title of an award + Non required: + * vintage_year (str) - vintage year in a format - `yyyy` + ##### Response + E.g. + ``` + { + "id": 1, + ... + } + ``` + """ queryset = Award.objects.all().with_base_related() serializer_class = serializers.BackAwardEmployeeCreateSerializer permission_classes = get_permission_classes() @@ -41,7 +115,52 @@ class AwardCreateAndBind(generics.CreateAPIView): class AwardRUDView(generics.RetrieveUpdateDestroyAPIView): - """Award RUD view.""" + """ + ## Retrieve/Update/Destroy Award view + ### *GET* + #### Description + Retrieving serialized object of an Award by an identifier + #### Response + E.g. + ``` + { + "id": 1, + ... + } + ``` + + ### *PATCH* + #### Description + Partially update Award object by identifier + ##### Request + Available: + * content_type (`int`) - identifier of content type entity + * object_id (`int`) - identifier of content object + * award_type (`int`) - identifier of award type + * title (`str`) - title of an award + * vintage_year (str) - vintage year in a format - `yyyy` + ##### Response + E.g. + ``` + { + "id": 1, + ... + } + ``` + + ### *DELETE* + #### Description + Delete an Award instance by award identifier + ##### Request + ``` + No request data + ``` + ##### Response + E.g. + ``` + No content + ``` + """ queryset = Award.objects.all().with_base_related() serializer_class = serializers.BackAwardSerializer permission_classes = get_permission_classes() diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index 03507ab0..3c88736b 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -32,6 +32,11 @@ class UserNotFoundError(AuthErrorMixin, ProjectBaseException): default_detail = _('User not found') +class EmployeeNotFoundError(ProjectBaseException): + """The exception should be thrown when the employee cannot get""" + default_detail = _('Employee not found') + + class EmailSendingError(exceptions.APIException): """The exception should be thrown when unable to send an email""" status_code = status.HTTP_400_BAD_REQUEST From c866721c8d5bb56978a36e77055e3f818379d427 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 7 Feb 2020 12:58:38 +0300 Subject: [PATCH 04/36] added a documentation for awards types --- apps/main/filters.py | 20 ++++++++++++++++ apps/main/models.py | 4 ++++ apps/main/views/back.py | 52 +++++++++++++++++++++++++---------------- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/apps/main/filters.py b/apps/main/filters.py index 72636d9e..065cfdd0 100644 --- a/apps/main/filters.py +++ b/apps/main/filters.py @@ -38,3 +38,23 @@ class AwardFilter(filters.FilterSet): if value not in EMPTY_VALUES: return queryset.by_employee_id(value, content_type='establishmentemployee') return queryset + + +class AwardTypeFilterSet(filters.FilterSet): + """Award type FilterSet.""" + + id = filters.NumberFilter(help_text='Filter by AwardType identifier.') + name = filters.CharFilter(method='by_name', help_text='Filter by AwardType name.') + + class Meta: + """Meta class.""" + model = models.AwardType + fields = [ + 'id', + 'name', + ] + + def by_name(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_name(value) + return queryset \ No newline at end of file diff --git a/apps/main/models.py b/apps/main/models.py index 639621a3..343b75d4 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -223,6 +223,10 @@ class AwardTypeQuerySet(models.QuerySet): """Filter QuerySet by country code.""" return self.filter(country__code=country_code) + def by_name(self, name: str): + """Filter by name field.""" + return self.filter(name__icontains=name) + class AwardType(models.Model): """AwardType model.""" diff --git a/apps/main/views/back.py b/apps/main/views/back.py index 8f264ac6..bf764076 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -9,7 +9,7 @@ from establishment.models import Employee from establishment.serializers.back import EmployeeBackSerializers from main import serializers from main import tasks -from main.filters import AwardFilter +from main.filters import AwardFilter, AwardTypeFilterSet from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature, AwardType from main.serializers.back import PanelSerializer from main.views import SiteSettingsView, SiteListView @@ -31,19 +31,19 @@ class AwardLstView(generics.ListCreateAPIView): * vintage_year (`str`) - Filter by a vintage year ##### Response E.g.: - ``` + ``` + { + "count": 58, + "next": 2, + "previous": null, + "results": [ { - "count": 58, - "next": 2, - "previous": null, - "results": [ - { - "id": 1, - ... - } - ] + "id": 1, + ... } - ``` + ] + } + ``` ### *POST* #### Description @@ -168,20 +168,32 @@ class AwardRUDView(generics.RetrieveUpdateDestroyAPIView): class AwardTypesListView(generics.ListAPIView): - """AwardType List view.""" + """ + ## List of Award types view. + ### *GET* + #### Description + Return non paginated list of Award types filtered by request country code. + Available filters: + * id (`int`) - award type identifier + * name (`str`) - award type name + ##### Response + ``` + [ + { + "id": 1, + ... + } + ] + ``` + """ pagination_class = None serializer_class = serializers.AwardTypeBaseSerializer permission_classes = get_permission_classes() - filter_backends = (DjangoFilterBackend,) - ordering_fields = '__all__' lookup_field = 'id' - filterset_fields = ( - 'id', - 'name', - ) + filter_class = AwardTypeFilterSet def get_queryset(self): - """Overridden get_queryset method.""" + """An overridden get_queryset method.""" if hasattr(self, 'request') and hasattr(self.request, 'country_code'): return AwardType.objects.by_country_code(self.request.country_code) return AwardType.objects.none() From 0eb6a73e096632e122f2fa8bcc782a43be44d0f5 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Fri, 7 Feb 2020 08:44:13 +0000 Subject: [PATCH 05/36] Carousel mixin --- apps/establishment/models.py | 18 ++++++++++++++---- apps/establishment/serializers/back.py | 6 ++++++ apps/news/models.py | 20 +++++++------------- apps/utils/models.py | 16 ++++++++++++++++ 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 60e3e0b5..ec8b57ef 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -33,7 +33,7 @@ from utils.models import ( BaseAttributes, FavoritesMixin, FileMixin, GalleryMixin, HasTagsMixin, IntermediateGalleryModelMixin, ProjectBaseMixin, TJSONField, TranslatedFieldsMixin, TypeDefaultImageMixin, URLImageMixin, default_menu_bool_array, PhoneModelMixin, - AwardsModelMixin) + AwardsModelMixin, CarouselMixin) # todo: establishment type&subtypes check @@ -547,8 +547,14 @@ class EstablishmentQuerySet(models.QuerySet): return self.prefetch_related('menu_set', 'menu_set__plates', 'back_office_wine') -class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, - TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin, AwardsModelMixin): +class Establishment(GalleryMixin, + ProjectBaseMixin, + URLImageMixin, + TranslatedFieldsMixin, + HasTagsMixin, + FavoritesMixin, + AwardsModelMixin, + CarouselMixin): """Establishment model.""" ABANDONED = 0 @@ -717,9 +723,13 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, raise ValidationError('Establishment type of subtype does not match') self.establishment_subtypes.add(establishment_subtype) + @property + def last_review(self): + return self.reviews.by_status(Review.READY).last() + @property def vintage_year(self): - last_review = self.reviews.by_status(Review.READY).last() + last_review = self.last_review if last_review: return last_review.vintage diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 9ac77045..3c218ed0 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -20,6 +20,7 @@ from location.serializers import AddressDetailSerializer, TranslatedField, Addre from main import models as main_models from main.models import Currency from main.serializers import AwardSerializer +from review.serializers import ReviewBaseSerializer from tag.serializers import TagBaseSerializer from utils.decorators import with_base_attributes from utils.methods import string_random @@ -211,6 +212,7 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): subtypes = model_serializers.EstablishmentSubTypeBaseSerializer(source='establishment_subtypes', read_only=True, many=True) type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) + phones = serializers.ListField( source='contact_phones', allow_null=True, @@ -219,8 +221,11 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): required=False, write_only=True, ) + contact_phones = ContactPhonesSerializer(source='phones', read_only=True, many=True) + last_review = ReviewBaseSerializer() + class Meta(model_serializers.EstablishmentBaseSerializer.Meta): fields = [ 'id', @@ -249,6 +254,7 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): 'tags', 'status', 'status_display', + 'last_review', ] def to_representation(self, instance): diff --git a/apps/news/models.py b/apps/news/models.py index e840f9c5..b24d442d 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -22,7 +22,7 @@ from utils.models import ( BaseAttributes, FavoritesMixin, GalleryMixin, HasTagsMixin, IntermediateGalleryModelMixin, ProjectBaseMixin, TJSONField, TranslatedFieldsMixin, TypeDefaultImageMixin, -) + CarouselMixin) from utils.querysets import TranslationQuerysetMixin @@ -257,8 +257,12 @@ class NewsQuerySet(TranslationQuerysetMixin): return self.filter(site__country__code=country_code) if not user.is_superuser else self -class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin, - FavoritesMixin): +class News(GalleryMixin, + BaseAttributes, + TranslatedFieldsMixin, + HasTagsMixin, + FavoritesMixin, + CarouselMixin): """News model.""" STR_FIELD_NAME = 'title' @@ -366,16 +370,6 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin, self.duplication_date = timezone.now() self.save() - @property - def must_of_the_week(self) -> bool: - """Detects whether current item in carousel""" - kwargs = { - 'content_type': ContentType.objects.get_for_model(self), - 'object_id': self.pk, - 'country': self.country, - } - return Carousel.objects.filter(**kwargs).exists() - @property def publication_datetime(self): """Represents datetime object combined from `publication_date` & `publication_time` fields""" diff --git a/apps/utils/models.py b/apps/utils/models.py index 2f2a39d1..06a1bf99 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -4,6 +4,7 @@ from os.path import exists from django.conf import settings from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.contrib.contenttypes.models import ContentType from django.contrib.gis.db import models from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import JSONField @@ -537,3 +538,18 @@ class AwardsModelMixin: if hasattr(self, 'awards'): self.awards.remove(award) + + +class CarouselMixin: + @property + def must_of_the_week(self) -> bool: + """Detects whether current item in carousel""" + from main.models import Carousel + + if hasattr(self, 'pk') and hasattr(self, 'country'): + kwargs = { + 'content_type': ContentType.objects.get_for_model(self), + 'object_id': self.pk, + 'country': self.country, + } + return Carousel.objects.filter(**kwargs).exists() \ No newline at end of file From 5b6db0f974a04f0d45bc76b0795a14d0d1bbb895 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Fri, 7 Feb 2020 10:40:30 +0000 Subject: [PATCH 06/36] must of the week mark --- apps/establishment/serializers/back.py | 1 + apps/utils/models.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 3c218ed0..cbedea4b 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -255,6 +255,7 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): 'status', 'status_display', 'last_review', + 'must_of_the_week', ] def to_representation(self, instance): diff --git a/apps/utils/models.py b/apps/utils/models.py index 06a1bf99..e61651b2 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -518,6 +518,7 @@ def default_menu_bool_array(): class PhoneModelMixin: """Mixin for PhoneNumberField.""" + @cached_property def country_calling_code(self): """Return phone code from PhonеNumberField.""" @@ -546,10 +547,13 @@ class CarouselMixin: """Detects whether current item in carousel""" from main.models import Carousel - if hasattr(self, 'pk') and hasattr(self, 'country'): + if hasattr(self, 'pk') and (hasattr(self, 'country') or hasattr(self, 'country_id')): kwargs = { 'content_type': ContentType.objects.get_for_model(self), 'object_id': self.pk, - 'country': self.country, + 'country': getattr(self, 'country', getattr(self, 'country_id', None)), } - return Carousel.objects.filter(**kwargs).exists() \ No newline at end of file + + return Carousel.objects.filter(**kwargs).exists() + + return False From 6feb67b0b5b7f7fc5550965f1bec9941e249f58a Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 7 Feb 2020 13:43:20 +0300 Subject: [PATCH 07/36] GET carousel for BO --- apps/establishment/serializers/back.py | 12 ++++++ apps/establishment/views/back.py | 60 ++++++++++++++++++++------ apps/main/serializers/back.py | 14 ++++++ apps/main/urls/back.py | 5 +-- apps/main/views/back.py | 41 +++++++++++++++++- 5 files changed, 115 insertions(+), 17 deletions(-) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 9ac77045..007131c9 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -355,6 +355,17 @@ class PositionBackSerializer(serializers.ModelSerializer): ] +class AdminEmployeeBackSerializers(serializers.ModelSerializer): + + class Meta: + model = models.Employee + fields = [ + 'id', + 'name', + 'last_name', + ] + + # TODO: test decorator @with_base_attributes class EmployeeBackSerializers(PhoneMixinSerializer, serializers.ModelSerializer): @@ -968,6 +979,7 @@ class CardAndWinesSerializer(serializers.ModelSerializer): class TeamMemberSerializer(serializers.ModelSerializer): """Serializer for team establishment BO section""" + class Meta: model = account_models.User fields = ( diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 591d38da..e25b083e 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -292,8 +292,8 @@ class CardAndWinesListView(generics.RetrieveAPIView): queryset = models.Establishment.objects.with_base_related() def get_object(self): - establishment = models.Establishment.objects.prefetch_plates()\ - .filter(pk=self.kwargs['establishment_id'])\ + establishment = models.Establishment.objects.prefetch_plates() \ + .filter(pk=self.kwargs['establishment_id']) \ .first() if establishment is None: raise Http404 @@ -705,7 +705,7 @@ class EmployeesListSearchViews(generics.ListAPIView): serializer_class = serializers.EmployeeBackSerializers queryset = ( models.Employee.objects.with_back_office_related() - .select_related('photo') + .select_related('photo') ) permission_classes = get_permission_classes( IsEstablishmentManager, @@ -827,6 +827,42 @@ class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): ) +class AdminEmployeeListView(generics.ListAPIView): + """ + ## Employee list view, where request user is admin. + ### *GET* + #### Description + Return paginated list of employees with available filters: + * search (`str`) - filter by name or last name with mistakes + * position_id (`int`) - filter by employees position identifier + * public_mark (`str`) - filter by establishment public mark + * toque_number (`str`) - filter by establishment toque number + * username (`str`) - filter by a username or name + + #### Response + ``` + { + "id": 1324, + "name": "Alex", + "establishment_id": 1324, + "establishment_type": 1324, + "establishment_subtype": 1324, + "establishment_slug": "slug", + { + ``` + + """ + serializer_class = serializers.EmployeeBackSerializers + queryset = models.Employee.objects.all().distinct().with_back_office_related() + permission_classes = get_permission_classes(IsEstablishmentAdministrator, ) + + def get_queryset(self): + qs = super().get_queryset() + if self.request.country_code: + qs = qs.filter(establishments__address__city__country__code=self.request.country_code) + return qs + + class RemoveAwardView(generics.DestroyAPIView): """ ## Remove award view. @@ -1226,10 +1262,10 @@ class EstablishmentGuideCreateDestroyView(generics.GenericAPIView): lookup_url_kwarg = getattr(self, 'establishment_lookup_url_kwarg', None) assert 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) + '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)} @@ -1247,10 +1283,10 @@ class EstablishmentGuideCreateDestroyView(generics.GenericAPIView): lookup_url_kwarg = getattr(self, 'guide_lookup_url_kwarg', None) assert 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) + '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)) @@ -1431,4 +1467,4 @@ class EstablishmentAwardCreateAndBind(generics.CreateAPIView, generics.DestroyAP def delete(self, request, *args, **kwargs): establishment = get_object_or_404(models.Establishment, id=kwargs['establishment_id']) establishment.remove_award(kwargs['award_id']) - return self._award_list_for_establishment(kwargs['establishment_id'], status.HTTP_200_OK) \ No newline at end of file + return self._award_list_for_establishment(kwargs['establishment_id'], status.HTTP_200_OK) diff --git a/apps/main/serializers/back.py b/apps/main/serializers/back.py index 58221fc0..6d57daef 100644 --- a/apps/main/serializers/back.py +++ b/apps/main/serializers/back.py @@ -3,6 +3,7 @@ from rest_framework import serializers from account.models import User from account.serializers import BackUserSerializer from main import models +from main.serializers import CarouselListSerializer class PanelSerializer(serializers.ModelSerializer): @@ -27,3 +28,16 @@ class PanelSerializer(serializers.ModelSerializer): 'user', 'user_id' ] + + +class BackCarouselListSerializer(CarouselListSerializer): + """Serializer for retrieving list of carousel items.""" + + class Meta: + """Meta class.""" + + model = models.Carousel + fields = CarouselListSerializer.Meta.fields + [ + 'active', + 'is_parse', + ] diff --git a/apps/main/urls/back.py b/apps/main/urls/back.py index 8b1cd6e4..b5f7b2f7 100644 --- a/apps/main/urls/back.py +++ b/apps/main/urls/back.py @@ -29,7 +29,6 @@ urlpatterns = [ path('panels//', views.PanelsRUDView.as_view(), name='panels-rud'), path('panels//execute/', views.PanelsExecuteView.as_view(), name='panels-execute'), path('panels//csv/', views.PanelsExportCSVView.as_view(), name='panels-csv'), - path('panels//xls/', views.PanelsExecuteXLSView.as_view(), name='panels-xls') + path('panels//xls/', views.PanelsExecuteXLSView.as_view(), name='panels-xls'), + path('carousel/', views.BackCarouselListView.as_view(), name='carousel-list'), ] - - diff --git a/apps/main/views/back.py b/apps/main/views/back.py index bf764076..5c73da5d 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -10,8 +10,8 @@ from establishment.serializers.back import EmployeeBackSerializers from main import serializers from main import tasks from main.filters import AwardFilter, AwardTypeFilterSet -from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature, AwardType -from main.serializers.back import PanelSerializer +from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature, AwardType, Carousel +from main.serializers.back import PanelSerializer, BackCarouselListSerializer from main.views import SiteSettingsView, SiteListView from utils.methods import get_permission_classes @@ -331,3 +331,40 @@ class PanelsExecuteXLSView(PanelsExecuteView): {"success": _('The file will be sent to your email.')}, status=status.HTTP_200_OK ) + + +class BackCarouselListView(generics.ListAPIView): + """ + ## List of carousel. + ### *GET* + #### Description + Return list of carousel items. + ##### Response + E.g.: + ``` + { + "id": 1, + "model_name": "model_name", + "name": "name", + ... + "awards": [ + { + "id": 1, + ... + } + ] + } + ``` + """ + + queryset = Carousel.objects.all() + serializer_class = BackCarouselListSerializer + permission_classes = get_permission_classes() + pagination_class = None + + def get_queryset(self): + country_code = self.request.country_code + qs = Carousel.objects.all() + if country_code: + qs = qs.by_country_code(country_code) + return qs From e4547b9554a3b162f274f001fc2351e182f5ea02 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 7 Feb 2020 14:09:53 +0300 Subject: [PATCH 08/36] active to carousel serializer --- apps/main/serializers/back.py | 1 - apps/utils/serializers.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/main/serializers/back.py b/apps/main/serializers/back.py index 6d57daef..1bf8b3bc 100644 --- a/apps/main/serializers/back.py +++ b/apps/main/serializers/back.py @@ -39,5 +39,4 @@ class BackCarouselListSerializer(CarouselListSerializer): model = models.Carousel fields = CarouselListSerializer.Meta.fields + [ 'active', - 'is_parse', ] diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index 2922d4cf..d609f6ec 100644 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -108,6 +108,7 @@ class CarouselCreateSerializer(serializers.ModelSerializer): model = Carousel fields = [ 'id', + 'active', ] @property From f9187c58012987bb61dc931919af3f3e6dc27c19 Mon Sep 17 00:00:00 2001 From: Kirill Date: Fri, 7 Feb 2020 14:24:34 +0300 Subject: [PATCH 09/36] remove collection and guide tags from metadata --- apps/tag/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/tag/filters.py b/apps/tag/filters.py index a9f675c6..a531e730 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -55,7 +55,7 @@ class TagCategoryFilterSet(TagsBaseFilterSet): if value == EstablishmentType.ARTISAN: qs = models.TagCategory.objects.with_base_related().filter(index_name='shop_category') else: - qs = queryset.by_establishment_type(value) + qs = queryset.by_establishment_type(value).exclude(index_name__in=['guide', 'collection']) return qs From 9084fbeab6da6491b03bb55ee7e274ea427b21a6 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 7 Feb 2020 14:25:50 +0300 Subject: [PATCH 10/36] reviews or bo list --- apps/establishment/models.py | 8 ++++++++ apps/establishment/serializers/back.py | 4 +++- apps/establishment/views/back.py | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 60e3e0b5..156f8f6e 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -137,6 +137,14 @@ class EstablishmentQuerySet(models.QuerySet): """Return qs with related reviews.""" return self.prefetch_related('reviews') + def with_reviews_sorted(self): + return self.prefetch_related( + Prefetch( + 'reviews', + queryset=Review.objects.published().order_by('-published_at'), + ) + ) + def with_currency_related(self): """Return qs with related """ return self.prefetch_related('currency') diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 007131c9..1c250a05 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -14,6 +14,7 @@ from collection.models import Guide from establishment import models, serializers as model_serializers from establishment.models import ContactEmail, ContactPhone, EstablishmentEmployee from establishment.serializers.common import ContactPhonesSerializer +from review.serializers.common import ReviewBaseSerializer from gallery.models import Image from location.serializers import AddressDetailSerializer, TranslatedField, AddressBaseSerializer, \ AddressEstablishmentSerializer @@ -92,7 +93,7 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria ) subtypes = model_serializers.EstablishmentSubTypeBaseSerializer(source='establishment_subtypes', read_only=True, many=True) - + reviews = ReviewBaseSerializer(allow_null=True, read_only=True, many=True) restaurant_category = TagBaseSerializer(read_only=True, many=True, allow_null=True) restaurant_cuisine = TagBaseSerializer(read_only=True, many=True, allow_null=True) artisan_category = TagBaseSerializer(read_only=True, many=True, allow_null=True) @@ -137,6 +138,7 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria 'artisan_category', 'distillery_type', 'food_producer', + 'reviews', ] def to_representation(self, instance): diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index e25b083e..ffc9f67e 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -65,7 +65,8 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP .with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \ .with_certain_tag_category_related('shop_category', 'artisan_category') \ .with_certain_tag_category_related('distillery_type', 'distillery_type') \ - .with_certain_tag_category_related('producer_type', 'food_producer') + .with_certain_tag_category_related('producer_type', 'food_producer') \ + .with_reviews_sorted() class EmployeeEstablishmentPositionsView(generics.ListAPIView): From f854cd6ba57f8d6f77f0b0a988f2d2bc7351ec4c Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Fri, 7 Feb 2020 11:41:49 +0000 Subject: [PATCH 11/36] last update by for establishment --- apps/establishment/models.py | 5 +++-- apps/establishment/serializers/back.py | 31 +++++++++++++++++++++++--- apps/utils/models.py | 11 +++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index ec8b57ef..2751987c 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -33,7 +33,7 @@ from utils.models import ( BaseAttributes, FavoritesMixin, FileMixin, GalleryMixin, HasTagsMixin, IntermediateGalleryModelMixin, ProjectBaseMixin, TJSONField, TranslatedFieldsMixin, TypeDefaultImageMixin, URLImageMixin, default_menu_bool_array, PhoneModelMixin, - AwardsModelMixin, CarouselMixin) + AwardsModelMixin, CarouselMixin, UpdateByMixin) # todo: establishment type&subtypes check @@ -554,7 +554,8 @@ class Establishment(GalleryMixin, HasTagsMixin, FavoritesMixin, AwardsModelMixin, - CarouselMixin): + CarouselMixin, + UpdateByMixin): """Establishment model.""" ABANDONED = 0 diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index cbedea4b..311cf028 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -1,3 +1,4 @@ +from datetime import datetime from functools import lru_cache from django.contrib.contenttypes.models import ContentType @@ -9,6 +10,7 @@ from rest_framework import serializers from slugify import slugify from account import models as account_models +from account.models import Role from account.serializers.common import UserShortSerializer from collection.models import Guide from establishment import models, serializers as model_serializers @@ -20,7 +22,7 @@ from location.serializers import AddressDetailSerializer, TranslatedField, Addre from main import models as main_models from main.models import Currency from main.serializers import AwardSerializer -from review.serializers import ReviewBaseSerializer +from review.serializers import ReviewBaseSerializer, User from tag.serializers import TagBaseSerializer from utils.decorators import with_base_attributes from utils.methods import string_random @@ -224,7 +226,7 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): contact_phones = ContactPhonesSerializer(source='phones', read_only=True, many=True) - last_review = ReviewBaseSerializer() + last_review = ReviewBaseSerializer(read_only=True) class Meta(model_serializers.EstablishmentBaseSerializer.Meta): fields = [ @@ -256,6 +258,8 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): 'status_display', 'last_review', 'must_of_the_week', + 'last_update_by_gm', + 'last_update_by_manager', ] def to_representation(self, instance): @@ -263,7 +267,7 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): data['phones'] = data.pop('contact_phones', None) return data - def update(self, instance, validated_data): + def update(self, instance: models.Establishment, validated_data): phones_list = [] if 'contact_phones' in validated_data: phones_list = validated_data.pop('contact_phones') @@ -272,9 +276,30 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): if 'contact_emails' in validated_data: emails_list = validated_data.pop('contact_emails') + request = self.context.get('request') + if request and hasattr(request, 'user'): + user = request.user + + if isinstance(user, User): + is_by_manager = user.userrole_set.filter( + pk=user.pk, + role__in=( + Role.ESTABLISHMENT_MANAGER, + Role.ESTABLISHMENT_ADMINISTRATOR, + Role.COUNTRY_ADMIN + ) + ).exists() + + if is_by_manager: + instance.last_update_by_manager = datetime.now() + else: + ''' by gm. ''' + instance.last_update_by_gm = datetime.now() + instance = super().update(instance, validated_data) phones_handler(phones_list, instance) emails_handler(emails_list, instance) + return instance diff --git a/apps/utils/models.py b/apps/utils/models.py index e61651b2..adbfbbeb 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -557,3 +557,14 @@ class CarouselMixin: return Carousel.objects.filter(**kwargs).exists() return False + + +class UpdateByMixin(models.Model): + """Modify by mixin""" + last_update_by_manager = models.DateTimeField(null=True) + + last_update_by_gm = models.DateTimeField(null=True) + + class Meta: + """Meta class.""" + abstract = True From 5023650e8b07e27ea54cc31c8e32d7974de925db Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Fri, 7 Feb 2020 11:43:50 +0000 Subject: [PATCH 12/36] migration for establishment --- .../migrations/0099_auto_20200207_1136.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 apps/establishment/migrations/0099_auto_20200207_1136.py diff --git a/apps/establishment/migrations/0099_auto_20200207_1136.py b/apps/establishment/migrations/0099_auto_20200207_1136.py new file mode 100644 index 00000000..4fac47e8 --- /dev/null +++ b/apps/establishment/migrations/0099_auto_20200207_1136.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.7 on 2020-02-07 11:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0098_auto_20200204_1205'), + ] + + operations = [ + migrations.AddField( + model_name='establishment', + name='last_update_by_gm', + field=models.DateTimeField(null=True), + ), + migrations.AddField( + model_name='establishment', + name='last_update_by_manager', + field=models.DateTimeField(null=True), + ), + ] From 955527a2c110b79e26afb056d4a831c7bc65733e Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 7 Feb 2020 15:02:58 +0300 Subject: [PATCH 13/36] employee for admin --- apps/establishment/urls/back.py | 1 + apps/establishment/views/back.py | 27 +++++++++++++-------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index 64bf736d..8882ba91 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -60,6 +60,7 @@ urlpatterns = [ path('/employees/', views.EstablishmentEmployeeListView.as_view(), name='establishment-employees'), path('employees/', views.EmployeeListCreateView.as_view(), name='employees'), + path('employees/for_admin/', views.AdminEmployeeListView.as_view(), name='employees-list-for-admin'), path('employees/search/', views.EmployeesListSearchViews.as_view(), name='employees-search'), path('employees//', views.EmployeeRUDView.as_view(), name='employees-rud'), path('employees//', views.RemoveAwardView.as_view(), name='employees-award-delete'), diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index e25b083e..9b73ac02 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -829,35 +829,34 @@ class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): class AdminEmployeeListView(generics.ListAPIView): """ - ## Employee list view, where request user is admin. + ## Employee list view, where request user is ESTABLISHMENT_ADMINISTRATOR. ### *GET* #### Description - Return paginated list of employees with available filters: - * search (`str`) - filter by name or last name with mistakes - * position_id (`int`) - filter by employees position identifier - * public_mark (`str`) - filter by establishment public mark - * toque_number (`str`) - filter by establishment toque number - * username (`str`) - filter by a username or name + Return paginated list of employees. #### Response ``` { "id": 1324, "name": "Alex", - "establishment_id": 1324, - "establishment_type": 1324, - "establishment_subtype": 1324, - "establishment_slug": "slug", + "last_name": "Wolf", { ``` """ - serializer_class = serializers.EmployeeBackSerializers - queryset = models.Employee.objects.all().distinct().with_back_office_related() + serializer_class = serializers.AdminEmployeeBackSerializers permission_classes = get_permission_classes(IsEstablishmentAdministrator, ) + pagination_class = None def get_queryset(self): - qs = super().get_queryset() + user = self.request.user + est_ids = models.Establishment.objects.filter( + userrole__user=user, + userrole__role__role=Role.ESTABLISHMENT_ADMINISTRATOR, + ).values_list('id', flat=True) + + qs = models.Employee.objects.filter(establishments__in=est_ids).distinct().with_back_office_related() + if self.request.country_code: qs = qs.filter(establishments__address__city__country__code=self.request.country_code) return qs From b7e64a0b263c9fc499b060c28483c94d57ff38fc Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 7 Feb 2020 15:09:11 +0300 Subject: [PATCH 14/36] is main establishment gallery --- apps/gallery/models.py | 6 ++++++ apps/gallery/serializers.py | 14 ++++++++++++-- apps/gallery/views.py | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/gallery/models.py b/apps/gallery/models.py index cc51af11..6933af55 100644 --- a/apps/gallery/models.py +++ b/apps/gallery/models.py @@ -69,6 +69,12 @@ class Image(BaseAttributes, SORLImageMixin, PlatformMixin): else: file_object.Acl().put(ACL='authenticated-read') + @property + def is_main(self) -> bool: + establishment_gallery_list = list(self.establishment_gallery.all()) + if establishment_gallery_list and len(establishment_gallery_list): + return establishment_gallery_list[0].is_main + @property def type(self) -> str: if self.image: diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index 0e50b231..dad6e2de 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -6,7 +6,7 @@ from sorl.thumbnail.parsers import parse_crop, ThumbnailParseError from django.utils.translation import gettext_lazy as _ from django.conf import settings from django.shortcuts import get_object_or_404 -from establishment.models import Establishment +from establishment.models import Establishment, EstablishmentGallery from account.serializers.common import UserBaseSerializer from . import models @@ -53,6 +53,7 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer): type = serializers.ChoiceField(read_only=True, choices=models.Image.MEDIA_TYPES) created_by = UserBaseSerializer(read_only=True, allow_null=True) image_size_in_KB = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=20) + is_main = serializers.BooleanField() class Meta: model = models.Image @@ -65,6 +66,7 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer): 'preview', 'is_public', 'title', + 'is_main', 'created_by', 'image_size_in_KB', ) @@ -82,20 +84,28 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer): return attrs def create(self, validated_data): + is_main = validated_data.pop('is_main') establishment = get_object_or_404(klass=Establishment, pk=self.context['view'].kwargs['establishment_id']) instance = super().create(validated_data) instance.created_by = self.context['request'].user instance.establishment_set.add(establishment) instance.save() + EstablishmentGallery.objects.filter( + image=instance + ).update(is_main=is_main) return instance def update(self, instance: models.Image, validated_data): if instance.is_public != validated_data.get('is_public'): instance.set_pubic(validated_data.get('is_public', True)) + if 'is_main' in validated_data: + is_main = validated_data.pop('is_main') + EstablishmentGallery.objects.filter( + image=instance + ).update(is_main=is_main) return super().update(instance, validated_data) - class CropImageSerializer(ImageSerializer): """Serializers for image crops.""" diff --git a/apps/gallery/views.py b/apps/gallery/views.py index 7dc99248..420af7a1 100644 --- a/apps/gallery/views.py +++ b/apps/gallery/views.py @@ -30,7 +30,7 @@ class MediaForEstablishmentView(ImageBaseView, generics.ListCreateAPIView): def get_queryset(self): return super().get_queryset().filter(establishment__pk=self.kwargs['establishment_id'])\ - .order_by('-order').prefetch_related('created_by') + .order_by('-order').prefetch_related('created_by', 'establishment_gallery') class MediaUpdateView(ImageBaseView, generics.UpdateAPIView): From 82e7262fdfe42b965cb47d6e83379bbd982abe23 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 7 Feb 2020 15:27:23 +0300 Subject: [PATCH 15/36] modified documentation strings for creating establishment schedule --- apps/establishment/views/back.py | 35 ++++++++++++++++++++++++++++---- apps/timetable/serialziers.py | 5 ----- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 3f0b635b..92e2f631 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -210,7 +210,7 @@ class EstablishmentRUDView(EstablishmentMixinViews, generics.RetrieveUpdateDestr ) def get_queryset(self): - """Overridden get_queryset method.""" + """An overridden get_queryset method.""" qs = super(EstablishmentRUDView, self).get_queryset() return qs.prefetch_related( 'establishmentemployee_set', @@ -271,9 +271,36 @@ class EstablishmentScheduleRUDView(EstablishmentMixinViews, generics.RetrieveUpd class EstablishmentScheduleCreateView(generics.CreateAPIView): """ - Establishment schedule Create view - - Implement creating Establishment shedule. + ## Create establishment schedule + ### *POST* + #### Description + Create schedule for establishment by establishment `slug`. + ##### Request + Required: + * weekday (`enum`) + ``` + 0 (Monday), + 1 (Tuesday), + 2 (Wednesday), + 3 (Thursday), + 4 (Friday), + 5 (Saturday), + 6 (Sunday) + ``` + Non-required: + * lunch_start (str) - time in a format (`ISO-8601`, e.g. - `hh:mm:ss`) + * lunch_end (str) - time in a format (`ISO-8601`, e.g. - `hh:mm:ss`) + * dinner_start (str) - time in a format (`ISO-8601`, e.g. - `hh:mm:ss`) + * dinner_end (str) - time in a format (`ISO-8601`, e.g. - `hh:mm:ss`) + * opening_at (str) - time in a format (`ISO-8601`, e.g. - `hh:mm:ss`) + * closed_at (str) - time in a format (`ISO-8601`, e.g. - `hh:mm:ss`) + ##### Response + ``` + { + "id": 1, + ... + } + ``` """ lookup_field = 'slug' serializer_class = ScheduleCreateSerializer diff --git a/apps/timetable/serialziers.py b/apps/timetable/serialziers.py index a3183543..a23de288 100644 --- a/apps/timetable/serialziers.py +++ b/apps/timetable/serialziers.py @@ -1,7 +1,5 @@ """Serializer for app timetable""" -import datetime - from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -23,8 +21,6 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): dinner_end = serializers.TimeField(required=False) opening_at = serializers.TimeField(required=False) closed_at = serializers.TimeField(required=False) - # For permission!! - establishment_id = serializers.ReadOnlyField(source='establishment.id') class Meta: """Meta class.""" @@ -40,7 +36,6 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): 'dinner_end', 'opening_at', 'closed_at', - 'establishment_id' ] def validate(self, attrs): From 31a9470da3b91bb89a9902438aeb69d7637cc92e Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 7 Feb 2020 15:51:54 +0300 Subject: [PATCH 16/36] allow any for establishment media --- apps/gallery/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/gallery/views.py b/apps/gallery/views.py index 420af7a1..c2408e0a 100644 --- a/apps/gallery/views.py +++ b/apps/gallery/views.py @@ -1,6 +1,7 @@ from django.conf import settings from django.db.transaction import on_commit from rest_framework import generics, status +from rest_framework.permissions import AllowAny from rest_framework.response import Response from utils.methods import get_permission_classes @@ -25,7 +26,8 @@ class ImageListCreateView(ImageBaseView, generics.ListCreateAPIView): class MediaForEstablishmentView(ImageBaseView, generics.ListCreateAPIView): """View for creating and retrieving certain establishment media.""" pagination_class = None - permission_classes = (IsCountryAdmin, IsEstablishmentAdministrator, IsEstablishmentManager, IsProducerFoodInspector) + # permission_classes = (IsCountryAdmin, IsEstablishmentAdministrator, IsEstablishmentManager, IsProducerFoodInspector) + permission_classes = (AllowAny, ) serializer_class = serializers.EstablishmentGallerySerializer def get_queryset(self): @@ -36,7 +38,8 @@ class MediaForEstablishmentView(ImageBaseView, generics.ListCreateAPIView): class MediaUpdateView(ImageBaseView, generics.UpdateAPIView): """View for updating media data""" serializer_class = serializers.EstablishmentGallerySerializer - permission_classes = () + # permission_classes = (IsCountryAdmin, IsEstablishmentAdministrator, IsEstablishmentManager, IsProducerFoodInspector) + permission_classes = (AllowAny, ) class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView): From 6645b590b87aa944510bc2cad730b1b1a9486148 Mon Sep 17 00:00:00 2001 From: Dmitry Borzenin Date: Fri, 7 Feb 2020 15:52:25 +0300 Subject: [PATCH 17/36] Add Parser for News slug --- apps/utils/parsers.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 apps/utils/parsers.py diff --git a/apps/utils/parsers.py b/apps/utils/parsers.py new file mode 100644 index 00000000..30ed754e --- /dev/null +++ b/apps/utils/parsers.py @@ -0,0 +1,43 @@ + + +class NewsSlug: + def __init__(self, value=None, locale=None, count=0): + self.value = value + self.locale = locale + self.count = count + + @classmethod + def parse(cls, raw_slug, country_codes): + slug, *rest = raw_slug.split('-') + instance = NewsSlug() + + if len(rest) >= 1 and rest[-1] in country_codes: + instance.value = '-'.join([slug, *rest[:-1]]) + instance.locale = rest[-1] + elif len(rest) >= 2 and rest[-1].isdigit() and rest[-2] in country_codes: + instance.value = '-'.join([slug, *rest[:-2]]) + instance.locale = rest[-2] + instance.count = int(rest[-1]) + else: + instance.value = '-'.join([slug, *rest]) + + return instance + + def __lt__(self, other): + return self.value < other.value + + def __str__(self): + if self.value is None: + raise ValueError('No value for slug') + + slug_parts = [self.value] + if self.locale is not None: + slug_parts.append(self.locale) + + if self.count != 0: + slug_parts.append(str(self.count)) + + return '-'.join(slug_parts) + + def __repr__(self): + return f'<{self.__class__.__name__} {self.value}, {self.locale}, {self.count}>' From 2fa6df0af84a4d50f307cf62482ef57e18a13550 Mon Sep 17 00:00:00 2001 From: Dmitry Borzenin Date: Fri, 7 Feb 2020 15:56:18 +0300 Subject: [PATCH 18/36] Fix news duplicate creation (unique slugs) --- apps/news/models.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/news/models.py b/apps/news/models.py index e840f9c5..da860c50 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -24,6 +24,8 @@ from utils.models import ( TJSONField, TranslatedFieldsMixin, TypeDefaultImageMixin, ) from utils.querysets import TranslationQuerysetMixin +from location.models import Country +from utils.parsers import NewsSlug class Agenda(ProjectBaseMixin, TranslatedFieldsMixin): @@ -358,9 +360,28 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin, return f'news: {next(iter(self.slugs.values()))}' def create_duplicate(self, new_country, view_count_model): + country_codes = list(Country.objects.all().values_list('code', flat=True)) + all_slugs = {slug_value + for slug_dict in News.objects.all().values_list('slugs', flat=True) + for slug_value in slug_dict.values()} + + new_slugs = {} + for locale, raw_slug in self.slugs.items(): + slug = NewsSlug.parse(raw_slug, country_codes) + similar_slugs = sorted(x for x in all_slugs if NewsSlug.parse(x, country_codes).value == slug.value) + if len(similar_slugs) == 0: + new_slugs[locale] = NewsSlug(slug.value, new_country.code) + else: + last_slug = NewsSlug.parse(similar_slugs[-1], country_codes) + new_slug = NewsSlug(slug.value, new_country.code, last_slug.count) + if last_slug.locale is not None: + new_slug.count += 1 + + new_slugs[locale] = str(new_slug) + self.pk = None self.state = self.UNPUBLISHED - self.slugs = {locale: f'{slug}-{new_country.code}' for locale, slug in self.slugs.items()} + self.slugs = new_slugs self.country = new_country self.views_count = view_count_model self.duplication_date = timezone.now() From a6cfa2ec58419dd33c245ab099e5716c01131534 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 7 Feb 2020 16:02:44 +0300 Subject: [PATCH 19/36] redefine EMAIL_TECHNICAL_SUPPORT variable in development use --- project/settings/development.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/project/settings/development.py b/project/settings/development.py index 9539daee..64c94221 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -1,5 +1,4 @@ """Development settings.""" -from .amazon_s3 import * from .base import * ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126', '0.0.0.0'] @@ -15,7 +14,6 @@ DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm.id-east.ru' - CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', @@ -26,7 +24,6 @@ CACHES = { } } - # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { @@ -35,7 +32,6 @@ ELASTICSEARCH_DSL = { } } - ELASTICSEARCH_INDEX_NAMES = { 'search_indexes.documents.news': 'development_news', 'search_indexes.documents.establishment': 'development_establishment', @@ -45,7 +41,6 @@ ELASTICSEARCH_INDEX_NAMES = { # ELASTICSEARCH_DSL_AUTOSYNC = False - # DATABASE DATABASES.update({ 'legacy': { @@ -75,7 +70,6 @@ EMAIL_HOST_USER = 'anatolyfeteleu@gmail.com' EMAIL_HOST_PASSWORD = 'nggrlnbehzksgmbt' EMAIL_PORT = 587 - MIDDLEWARE.append('utils.middleware.log_db_queries_per_API_request') LOGGING = { @@ -107,3 +101,5 @@ LOGGING = { }, } } + +EMAIL_TECHNICAL_SUPPORT = 'n.malinova@octopod.ru' From d488b7b22929e0a469cb7e8860ebead999440954 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 7 Feb 2020 16:28:11 +0300 Subject: [PATCH 20/36] fix anonymous user --- apps/establishment/views/back.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 92e2f631..4f1afaa5 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -878,6 +878,8 @@ class AdminEmployeeListView(generics.ListAPIView): def get_queryset(self): user = self.request.user + if user.is_anonymous: + return None est_ids = models.Establishment.objects.filter( userrole__user=user, userrole__role__role=Role.ESTABLISHMENT_ADMINISTRATOR, From f30a893faec62edef541ffbfa97ffeb2cf7a9655 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 7 Feb 2020 16:41:19 +0300 Subject: [PATCH 21/36] establishment vintage year and delete media --- apps/establishment/serializers/back.py | 2 ++ apps/gallery/views.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 1369fa00..2cca450c 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -102,6 +102,7 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria artisan_category = TagBaseSerializer(read_only=True, many=True, allow_null=True) distillery_type = TagBaseSerializer(read_only=True, many=True, allow_null=True) food_producer = TagBaseSerializer(read_only=True, many=True, allow_null=True) + vintage_year = serializers.IntegerField(read_only=True, allow_null=True) class Meta(model_serializers.EstablishmentBaseSerializer.Meta): fields = [ @@ -142,6 +143,7 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria 'distillery_type', 'food_producer', 'reviews', + 'vintage_year', ] def to_representation(self, instance): diff --git a/apps/gallery/views.py b/apps/gallery/views.py index c2408e0a..80b89cea 100644 --- a/apps/gallery/views.py +++ b/apps/gallery/views.py @@ -35,12 +35,21 @@ class MediaForEstablishmentView(ImageBaseView, generics.ListCreateAPIView): .order_by('-order').prefetch_related('created_by', 'establishment_gallery') -class MediaUpdateView(ImageBaseView, generics.UpdateAPIView): +class MediaUpdateView(ImageBaseView, generics.UpdateAPIView, generics.DestroyAPIView): """View for updating media data""" serializer_class = serializers.EstablishmentGallerySerializer # permission_classes = (IsCountryAdmin, IsEstablishmentAdministrator, IsEstablishmentManager, IsProducerFoodInspector) permission_classes = (AllowAny, ) + def delete(self, request, *args, **kwargs): + """Override destroy view""" + instance = self.get_object() + if settings.USE_CELERY: + on_commit(lambda: tasks.delete_image.delay(image_id=instance.id)) + else: + on_commit(lambda: tasks.delete_image(image_id=instance.id)) + return Response(status=status.HTTP_204_NO_CONTENT) + class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView): """Destroy view for model Image""" From 73a5f9dc0937bd9312d864e799f97e4c17699cbf Mon Sep 17 00:00:00 2001 From: Dmitry Borzenin Date: Fri, 7 Feb 2020 16:51:22 +0300 Subject: [PATCH 22/36] Refactoring in NewsSlag parser --- apps/utils/parsers.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/utils/parsers.py b/apps/utils/parsers.py index 30ed754e..073aa1e4 100644 --- a/apps/utils/parsers.py +++ b/apps/utils/parsers.py @@ -1,9 +1,9 @@ class NewsSlug: - def __init__(self, value=None, locale=None, count=0): + def __init__(self, value=None, country_code=None, count=0): self.value = value - self.locale = locale + self.country_code = country_code self.count = count @classmethod @@ -13,10 +13,10 @@ class NewsSlug: if len(rest) >= 1 and rest[-1] in country_codes: instance.value = '-'.join([slug, *rest[:-1]]) - instance.locale = rest[-1] + instance.country_code = rest[-1] elif len(rest) >= 2 and rest[-1].isdigit() and rest[-2] in country_codes: instance.value = '-'.join([slug, *rest[:-2]]) - instance.locale = rest[-2] + instance.country_code = rest[-2] instance.count = int(rest[-1]) else: instance.value = '-'.join([slug, *rest]) @@ -31,8 +31,8 @@ class NewsSlug: raise ValueError('No value for slug') slug_parts = [self.value] - if self.locale is not None: - slug_parts.append(self.locale) + if self.country_code is not None: + slug_parts.append(self.country_code) if self.count != 0: slug_parts.append(str(self.count)) @@ -40,4 +40,4 @@ class NewsSlug: return '-'.join(slug_parts) def __repr__(self): - return f'<{self.__class__.__name__} {self.value}, {self.locale}, {self.count}>' + return f'<{self.__class__.__name__} {self.value}, {self.country_code}, {self.count}>' From 06137fcccfee93b102897f99808cfee0bc8e9188 Mon Sep 17 00:00:00 2001 From: Dmitry Borzenin Date: Fri, 7 Feb 2020 16:52:48 +0300 Subject: [PATCH 23/36] Refactoring in News.create_duplicate --- apps/news/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/news/models.py b/apps/news/models.py index da860c50..7c585148 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -370,11 +370,12 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin, slug = NewsSlug.parse(raw_slug, country_codes) similar_slugs = sorted(x for x in all_slugs if NewsSlug.parse(x, country_codes).value == slug.value) if len(similar_slugs) == 0: - new_slugs[locale] = NewsSlug(slug.value, new_country.code) + # It is impossible because at least current instance has slug + raise ValueError('Duplicating unsaved object') else: last_slug = NewsSlug.parse(similar_slugs[-1], country_codes) new_slug = NewsSlug(slug.value, new_country.code, last_slug.count) - if last_slug.locale is not None: + if last_slug.country_code is not None: new_slug.count += 1 new_slugs[locale] = str(new_slug) From afc39a6257f80ce3d508876317cb4fcf052628d4 Mon Sep 17 00:00:00 2001 From: Dmitry Borzenin Date: Fri, 7 Feb 2020 16:53:25 +0300 Subject: [PATCH 24/36] Add comments on create_duplicate and NewsSlug parser --- apps/news/models.py | 7 +++++++ apps/utils/parsers.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/apps/news/models.py b/apps/news/models.py index 7c585148..24c5df11 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -361,6 +361,8 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin, def create_duplicate(self, new_country, view_count_model): country_codes = list(Country.objects.all().values_list('code', flat=True)) + + # Get all existed slugs all_slugs = {slug_value for slug_dict in News.objects.all().values_list('slugs', flat=True) for slug_value in slug_dict.values()} @@ -368,12 +370,17 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin, new_slugs = {} for locale, raw_slug in self.slugs.items(): slug = NewsSlug.parse(raw_slug, country_codes) + + # all slugs LIKE% slug similar_slugs = sorted(x for x in all_slugs if NewsSlug.parse(x, country_codes).value == slug.value) + if len(similar_slugs) == 0: # It is impossible because at least current instance has slug raise ValueError('Duplicating unsaved object') else: + # The last slug in similar_slugs is slug with largest count last_slug = NewsSlug.parse(similar_slugs[-1], country_codes) + new_slug = NewsSlug(slug.value, new_country.code, last_slug.count) if last_slug.country_code is not None: new_slug.count += 1 diff --git a/apps/utils/parsers.py b/apps/utils/parsers.py index 073aa1e4..dc84a60e 100644 --- a/apps/utils/parsers.py +++ b/apps/utils/parsers.py @@ -12,13 +12,19 @@ class NewsSlug: instance = NewsSlug() if len(rest) >= 1 and rest[-1] in country_codes: + # It is like 'slug-en' + instance.value = '-'.join([slug, *rest[:-1]]) instance.country_code = rest[-1] elif len(rest) >= 2 and rest[-1].isdigit() and rest[-2] in country_codes: + # It is like 'slug-en-1' + instance.value = '-'.join([slug, *rest[:-2]]) instance.country_code = rest[-2] instance.count = int(rest[-1]) else: + # It is like 'slug' + instance.value = '-'.join([slug, *rest]) return instance From af6c2a735ee5b3317ab20418712c618d1fccb193 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 7 Feb 2020 16:56:57 +0300 Subject: [PATCH 25/36] amazon fix --- project/settings/development.py | 1 + 1 file changed, 1 insertion(+) diff --git a/project/settings/development.py b/project/settings/development.py index 64c94221..1fdda847 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -1,5 +1,6 @@ """Development settings.""" from .base import * +from .amazon_s3 import * ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126', '0.0.0.0'] From 969fde01b346cd9a776cf0078aad72b194c0c4b0 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 7 Feb 2020 16:58:08 +0300 Subject: [PATCH 26/36] amazon fix --- project/settings/development.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/settings/development.py b/project/settings/development.py index 1fdda847..691726b1 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -1,6 +1,6 @@ """Development settings.""" -from .base import * from .amazon_s3 import * +from .base import * ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126', '0.0.0.0'] From aa614865ab517ee22f82be3f9fb2ec1bfe6d6e3b Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 7 Feb 2020 17:03:21 +0300 Subject: [PATCH 27/36] amazon fix --- project/settings/development.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project/settings/development.py b/project/settings/development.py index 691726b1..25736d95 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -1,5 +1,4 @@ """Development settings.""" -from .amazon_s3 import * from .base import * ALLOWED_HOSTS = ['gm.id-east.ru', '95.213.204.126', '0.0.0.0'] @@ -104,3 +103,5 @@ LOGGING = { } EMAIL_TECHNICAL_SUPPORT = 'n.malinova@octopod.ru' + +from .amazon_s3 import * From 4559b0eeeb72eb9e8e48541c7dcd7f3aae9d1562 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 7 Feb 2020 17:28:30 +0300 Subject: [PATCH 28/36] added a documentation for gallery --- apps/establishment/views/back.py | 58 ++++++++++++++++++++++++++++++-- apps/gallery/views.py | 50 ++++++++++++++++++++++++++- apps/news/views.py | 56 ++++++++++++++++++++++++++++-- apps/product/views/back.py | 56 ++++++++++++++++++++++++++++-- 4 files changed, 212 insertions(+), 8 deletions(-) diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 4f1afaa5..4331b502 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -1021,7 +1021,38 @@ class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews, CreateDestroyGalleryViewMixin): - """Resource for a create|destroy gallery for establishment for back-office users.""" + """ + ## Establishment gallery image Create/Destroy view + ### *POST* + #### Description + Attaching existing **image** by `image identifier` to **establishment** by `establishment slug` + in request kwargs. + ##### Request + ``` + No body + ``` + ##### Response + E.g.: + ``` + No content + ``` + + ### *DELETE* + #### Description + Delete existing **gallery image** from **establishment** gallery, by `image identifier` + and `establishment slug` in request kwargs. + + **Note**: + > Image wouldn't be deleted after all. + ##### Request + ``` + No body + ``` + ##### Response + ``` + No content + ``` + """ lookup_field = 'slug' serializer_class = serializers.EstablishmentBackOfficeGallerySerializer permission_classes = get_permission_classes() @@ -1044,7 +1075,28 @@ class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews, class EstablishmentGalleryListView(EstablishmentMixinViews, generics.ListAPIView): - """Resource for returning gallery for establishment for back-office users.""" + """ + ## Establishment gallery image list view + ### *GET* + #### Description + Returning paginated list of establishment images by `establishment slug`, + with cropped images. + ##### Response + E.g.: + ``` + { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 11, + ... + } + ] + } + ``` + """ lookup_field = 'slug' serializer_class = serializers.ImageBaseSerializer permission_classes = get_permission_classes() @@ -1054,7 +1106,7 @@ class EstablishmentGalleryListView(EstablishmentMixinViews, qs = super(EstablishmentGalleryListView, self).get_queryset() establishment = get_object_or_404(qs, slug=self.kwargs.get('slug')) - # May raise a permission denied + # May raises a permission denied self.check_object_permissions(self.request, establishment) return establishment diff --git a/apps/gallery/views.py b/apps/gallery/views.py index 80b89cea..45192530 100644 --- a/apps/gallery/views.py +++ b/apps/gallery/views.py @@ -9,6 +9,7 @@ from utils.permissions import IsContentPageManager, IsCountryAdmin, IsEstablishm IsProducerFoodInspector, IsEstablishmentAdministrator from . import tasks, models, serializers + class ImageBaseView(generics.GenericAPIView): """Base Image view.""" model = models.Image @@ -20,7 +21,54 @@ class ImageBaseView(generics.GenericAPIView): class ImageListCreateView(ImageBaseView, generics.ListCreateAPIView): - """List/Create Image view.""" + """ + ## List/Create view + ### *GET* + #### Description + Get paginated list of images, with ordering by field `modified` (descending) + #### Response + E.g.: + ``` + { + "count": 40595, + "next": 2, + "previous": null, + "results": [ + { + "id": 47336, + ... + } + ] + } + ``` + ### *POST* + #### Description + Upload an image on a server. + ##### Request + Required: + * file (`file`) - download file + Available: + * orientation (`enum`) - default: `null` + ``` + 0 (Horizontal) + 1 (Vertical) + ``` + * title (`str`) - title of image file (default - `''`) + * is_public (`bool`) - flag that responds for availability + for displaying (default - `True`) + * preview (`file`) - download preview file (default - `null`) + * link (`str`) - mp4 or youtube video link (default - `null`) + * order (`int`) - order number (default - `0`) + ##### Response + E.g.: + ``` + { + "id": 47336, + ... + } + + ``` + """ class MediaForEstablishmentView(ImageBaseView, generics.ListCreateAPIView): diff --git a/apps/news/views.py b/apps/news/views.py index b0c11e08..5071d79e 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -176,7 +176,38 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView, class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, CreateDestroyGalleryViewMixin): - """Resource for a create gallery for news for back-office users.""" + """ + ## News gallery image Create/Destroy view + ### *POST* + #### Description + Attaching existing **image** by `image identifier` to **news** by `news identifier` + in request kwargs. + ##### Request + ``` + No body + ``` + ##### Response + E.g.: + ``` + No content + ``` + + ### *DELETE* + #### Description + Delete existing **gallery image** from **news** gallery, by `image identifier` + and `news identifier` in request kwargs. + + **Note**: + > Image wouldn't be deleted after all. + ##### Request + ``` + No body + ``` + ##### Response + ``` + No content + ``` + """ serializer_class = serializers.NewsBackOfficeGallerySerializer def create(self, request, *args, **kwargs): @@ -203,7 +234,28 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIView): - """Resource for returning gallery for news for back-office users.""" + """ + ## News gallery image list view + ### *GET* + #### Description + Returning paginated list of news images by `news identifier`, + with cropped images. + ##### Response + E.g.: + ``` + { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 11, + ... + } + ] + } + ``` + """ serializer_class = ImageBaseSerializer def get_object(self): diff --git a/apps/product/views/back.py b/apps/product/views/back.py index a660d242..a1c7fee0 100644 --- a/apps/product/views/back.py +++ b/apps/product/views/back.py @@ -46,7 +46,38 @@ class ProductSubTypeBackOfficeMixinView: class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView, CreateDestroyGalleryViewMixin): - """Resource for a create gallery for product for back-office users.""" + """ + ## Product gallery image Create/Destroy view + ### *POST* + #### Description + Attaching existing **image** by `image identifier` to **product** by `product identifier` + in request kwargs. + ##### Request + ``` + No body + ``` + ##### Response + E.g.: + ``` + No content + ``` + + ### *DELETE* + #### Description + Delete existing **gallery image** from **product** gallery, by `image identifier` + and `product identifier` in request kwargs. + + **Note**: + > Image wouldn't be deleted after all. + ##### Request + ``` + No body + ``` + ##### Response + ``` + No content + ``` + """ serializer_class = serializers.ProductBackOfficeGallerySerializer def get_object(self): @@ -66,7 +97,28 @@ class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView, class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.ListAPIView): - """Resource for returning gallery for product for back-office users.""" + """ + ## Product gallery image list view + ### *GET* + #### Description + Returning paginated list of product images by `product identifier`, + with cropped images. + ##### Response + E.g.: + ``` + { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 11, + ... + } + ] + } + ``` + """ serializer_class = ImageBaseSerializer def get_object(self): From 756211b8d19e1b39c288ca7999b383e611336461 Mon Sep 17 00:00:00 2001 From: Dmitry Borzenin Date: Fri, 7 Feb 2020 17:45:49 +0300 Subject: [PATCH 29/36] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 2dabe48f..681e23bd 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,11 @@ logs/ /datadir/ /_files/ /geoip_db/ +/venv # dev ./docker-compose.override.yml +docker-compose.override.yml celerybeat-schedule local_files celerybeat.pid From 35d94e6f765077db6374344f25b91bbcdd671688 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 7 Feb 2020 18:07:17 +0300 Subject: [PATCH 30/36] always one is_main media item --- apps/gallery/serializers.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index dad6e2de..e1a259a1 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -90,6 +90,10 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer): instance.created_by = self.context['request'].user instance.establishment_set.add(establishment) instance.save() + if is_main: + EstablishmentGallery.objects.filter( + establishment=establishment + ).update(is_main=False) # reset all before setting True on some instance EstablishmentGallery.objects.filter( image=instance ).update(is_main=is_main) @@ -100,6 +104,11 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer): instance.set_pubic(validated_data.get('is_public', True)) if 'is_main' in validated_data: is_main = validated_data.pop('is_main') + if is_main: + establishment = instance.establishment_gallery.all()[0].establishment + EstablishmentGallery.objects.filter( + establishment=establishment + ).update(is_main=False) # reset all before setting True on some instance EstablishmentGallery.objects.filter( image=instance ).update(is_main=is_main) From 851c9fc830c6a7e4935d0d7299ecf731cff9e8de Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 7 Feb 2020 18:09:52 +0300 Subject: [PATCH 31/36] establishment back list --- apps/review/models.py | 4 ++++ apps/review/serializers/common.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/apps/review/models.py b/apps/review/models.py index f17629ba..34f2c4b7 100644 --- a/apps/review/models.py +++ b/apps/review/models.py @@ -97,6 +97,10 @@ class Review(BaseAttributes, TranslatedFieldsMixin): objects = ReviewQuerySet.as_manager() + @property + def status_display(self): + return self.REVIEW_STATUSES[self.status][1] + class Meta: """Meta class.""" verbose_name = _('Review') diff --git a/apps/review/serializers/common.py b/apps/review/serializers/common.py index e714fff7..46a9741d 100644 --- a/apps/review/serializers/common.py +++ b/apps/review/serializers/common.py @@ -4,14 +4,19 @@ from review.models import Review, Inquiries, GridItems class ReviewBaseSerializer(serializers.ModelSerializer): + text_translated = serializers.CharField(read_only=True) + status_display = serializers.CharField(read_only=True) + class Meta: model = Review fields = ( 'id', 'reviewer', 'text', + 'text_translated', 'priority', 'status', + 'status_display', 'child', 'published_at', 'vintage', From 3d136d9b8a79234c3ced228645b5db9e24dd9eb8 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 7 Feb 2020 18:16:09 +0300 Subject: [PATCH 32/36] more detailed address for establishment back list --- apps/location/serializers/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index 8c9cf07b..9221c3b6 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -234,6 +234,7 @@ class AddressEstablishmentSerializer(AddressBaseSerializer): write_only=True, required=True, ) + city = CityBaseSerializer(read_only=True) class Meta(AddressBaseSerializer.Meta): """Meta class.""" @@ -245,6 +246,7 @@ class AddressEstablishmentSerializer(AddressBaseSerializer): 'number', 'postal_code', 'city_id', + 'city', ) From 4077288f027249c9acb2a4be2bac2ab6f7e2b4dd Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 7 Feb 2020 18:20:09 +0300 Subject: [PATCH 33/36] sorting for establishment media gallery --- apps/gallery/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/gallery/views.py b/apps/gallery/views.py index 45192530..da0cac21 100644 --- a/apps/gallery/views.py +++ b/apps/gallery/views.py @@ -80,7 +80,8 @@ class MediaForEstablishmentView(ImageBaseView, generics.ListCreateAPIView): def get_queryset(self): return super().get_queryset().filter(establishment__pk=self.kwargs['establishment_id'])\ - .order_by('-order').prefetch_related('created_by', 'establishment_gallery') + .order_by('-establishment_gallery__is_main', '-order').prefetch_related('created_by', + 'establishment_gallery') class MediaUpdateView(ImageBaseView, generics.UpdateAPIView, generics.DestroyAPIView): From 3da4dfd54bc90450d5b5e90d008aea8ac6bb7f60 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Fri, 7 Feb 2020 15:41:57 +0000 Subject: [PATCH 34/36] hide useless statuses --- apps/establishment/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 020b7e89..611830fb 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -580,12 +580,12 @@ class Establishment(GalleryMixin, (ABANDONED, _('Abandoned')), (CLOSED, _('Closed')), (PUBLISHED, _('Published')), - (UNPICKED, _('Unpicked')), + # (UNPICKED, _('Unpicked')), (WAITING, _('Waiting')), - (HIDDEN, _('Hidden')), - (DELETED, _('Deleted')), + # (HIDDEN, _('Hidden')), + # (DELETED, _('Deleted')), (OUT_OF_SELECTION, _('Out of selection')), - (UNPUBLISHED, _('Unpublished')), + # (UNPUBLISHED, _('Unpublished')), ) old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) From 8f9b1f81453bf787cb87acbef10c0f6e4b84e780 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Sat, 8 Feb 2020 22:16:13 +0300 Subject: [PATCH 35/36] cropbox image --- .../migrations/0010_auto_20200206_1944.py | 18 +++++++++++++++++ apps/gallery/migrations/0011_image_cropbox.py | 20 +++++++++++++++++++ apps/gallery/models.py | 14 +++++++++++++ apps/gallery/serializers.py | 7 ++++++- 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 apps/gallery/migrations/0010_auto_20200206_1944.py create mode 100644 apps/gallery/migrations/0011_image_cropbox.py diff --git a/apps/gallery/migrations/0010_auto_20200206_1944.py b/apps/gallery/migrations/0010_auto_20200206_1944.py new file mode 100644 index 00000000..218aa2c6 --- /dev/null +++ b/apps/gallery/migrations/0010_auto_20200206_1944.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2020-02-06 19:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gallery', '0009_auto_20200206_1749'), + ] + + operations = [ + migrations.AlterField( + model_name='image', + name='is_public', + field=models.BooleanField(default=True, verbose_name='Is media source public'), + ), + ] diff --git a/apps/gallery/migrations/0011_image_cropbox.py b/apps/gallery/migrations/0011_image_cropbox.py new file mode 100644 index 00000000..26f8d569 --- /dev/null +++ b/apps/gallery/migrations/0011_image_cropbox.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.7 on 2020-02-08 19:00 + +import django.core.validators +from django.db import migrations, models +import re + + +class Migration(migrations.Migration): + + dependencies = [ + ('gallery', '0010_auto_20200206_1944'), + ] + + operations = [ + migrations.AddField( + model_name='image', + name='cropbox', + field=models.CharField(default=None, max_length=500, null=True, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:,\\d+)*\\Z'), code='invalid', message='Enter only digits separated by commas.')], verbose_name='x1,y1,x2,y2 crop settings'), + ), + ] diff --git a/apps/gallery/models.py b/apps/gallery/models.py index 6933af55..6a19b859 100644 --- a/apps/gallery/models.py +++ b/apps/gallery/models.py @@ -1,5 +1,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from django.core import validators +from sorl.thumbnail import get_thumbnail from botocore.exceptions import ClientError from django.conf import settings from project.storage_backends import PublicMediaStorage @@ -43,6 +45,8 @@ class Image(BaseAttributes, SORLImageMixin, PlatformMixin): default=None) link = models.URLField(blank=True, null=True, default=None, verbose_name=_('mp4 or youtube video link')) order = models.PositiveIntegerField(default=0, verbose_name=_('Sorting order')) + cropbox = models.CharField(max_length=500, validators=[validators.validate_comma_separated_integer_list], null=True, + default=None, verbose_name=_('x1,y1,x2,y2 crop settings')) objects = ImageQuerySet.as_manager() class Meta: @@ -55,6 +59,16 @@ class Image(BaseAttributes, SORLImageMixin, PlatformMixin): """String representation""" return f'{self.id}' + @property + def image_by_cropbox(self): + """Returns cropped image if cropbox is set""" + if self.cropbox and self.image: + x1, y1, x2, y2 = map(int, self.cropbox.split(',')) + return get_thumbnail(self.image, + geometry_string=f'{round(x2 - x1)}x{round(y2 - y1)}', + cropbox=self.cropbox, + quality=100) + def set_pubic(self, is_public=True): if not settings.AWS_STORAGE_BUCKET_NAME: """Backend doesn't use aws s3""" diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index e1a259a1..4b3e4832 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -54,6 +54,7 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer): created_by = UserBaseSerializer(read_only=True, allow_null=True) image_size_in_KB = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=20) is_main = serializers.BooleanField() + cropped_image = serializers.ImageField(source='image_by_cropbox', allow_null=True, read_only=True) class Meta: model = models.Image @@ -69,6 +70,8 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer): 'is_main', 'created_by', 'image_size_in_KB', + 'cropbox', + 'cropped_image', ) extra_kwargs = { 'created': {'read_only': True}, @@ -80,7 +83,9 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer): if image and image.size >= settings.FILE_UPLOAD_MAX_MEMORY_SIZE: raise serializers.ValidationError({'detail': _('File size too large: %s bytes') % image.size}) - + if attrs.get('cropbox'): + if len(attrs['cropbox'].split(',')) != 4: + raise serializers.ValidationError({'detail': _('Cropbox contains 4 integer values separated by comma.')}) return attrs def create(self, validated_data): From e2e3285b6634e9ae49b66e6639f1ce13dfd783ea Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Sun, 9 Feb 2020 19:16:36 +0300 Subject: [PATCH 36/36] add created & created by --- apps/gallery/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index 4b3e4832..2667d7bc 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -69,12 +69,14 @@ class EstablishmentGallerySerializer(serializers.ModelSerializer): 'title', 'is_main', 'created_by', + 'created', 'image_size_in_KB', 'cropbox', 'cropped_image', ) extra_kwargs = { 'created': {'read_only': True}, + 'created_by': {'read_only': True}, } def validate(self, attrs):