From 6e91a2ef92fb8b0f8d24887e0ad6aa5e2184f907 Mon Sep 17 00:00:00 2001 From: dormantman Date: Sat, 11 Jan 2020 07:27:37 +0300 Subject: [PATCH 001/119] Added non-required field agenda for methods --- apps/news/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 2c7983d1..a803e009 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -180,6 +180,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): """News back office base serializer.""" is_published = serializers.BooleanField(source='is_publish', read_only=True) descriptions = serializers.ListField(required=False) + agenda = serializers.RelatedField(many=True, queryset=models.Agenda.objects.all(), required=False) class Meta(NewsBaseSerializer.Meta): """Meta class.""" @@ -198,6 +199,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): 'created', 'modified', 'descriptions', + 'agenda' ) extra_kwargs = { 'created': {'read_only': True}, From bcf73897c87a885edc2ea4dc2b90e51787285163 Mon Sep 17 00:00:00 2001 From: dormantman Date: Sat, 11 Jan 2020 07:49:50 +0300 Subject: [PATCH 002/119] Added agenda validation --- apps/news/serializers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index a803e009..973c4c8e 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -230,6 +230,13 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): for locale in locales: if not attrs[key].get(locale): attrs[key][locale] = getattr(instance, key).get(locale) + + if 'agenda' in attrs: + agenda = models.Agenda.objects.get(attrs['agenda']) + + if agenda.event_name is None or agenda.content is None: + raise serializers.ValidationError({'agenda': _('Agenda is empty')}) + return attrs def create(self, validated_data): From 62a66a80fb7a19a20001b77d7f45faf5d6d6962b Mon Sep 17 00:00:00 2001 From: dormantman Date: Sat, 11 Jan 2020 07:56:21 +0300 Subject: [PATCH 003/119] Added agenda validation in all methods --- apps/news/serializers.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 973c4c8e..77499b84 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -231,12 +231,6 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): if not attrs[key].get(locale): attrs[key][locale] = getattr(instance, key).get(locale) - if 'agenda' in attrs: - agenda = models.Agenda.objects.get(attrs['agenda']) - - if agenda.event_name is None or agenda.content is None: - raise serializers.ValidationError({'agenda': _('Agenda is empty')}) - return attrs def create(self, validated_data): @@ -254,6 +248,10 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): user = request.user validated_data['created_by'] = user + agenda = validated_data.get('agenda') + if agenda is not None and (agenda.event_name is None or agenda.content is None): + raise serializers.ValidationError({'agenda': _('Agenda is empty')}) + return super().create(validated_data) def update(self, instance, validated_data): @@ -265,6 +263,11 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): slugs__values__contains=list(slugs.values()) ).exists() or len(slugs_list) != len(slugs_set): raise serializers.ValidationError({'slugs': _('Slug should be unique')}) + + agenda = validated_data.get('agenda') + if agenda is not None and (agenda.event_name is None or agenda.content is None): + raise serializers.ValidationError({'agenda': _('Agenda is empty')}) + return super().update(instance, validated_data) From 605a3e12e1a94f61057c78a06d439e3af4b19c66 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 13 Jan 2020 15:44:51 +0300 Subject: [PATCH 004/119] phone_list field create and update --- apps/establishment/serializers/back.py | 52 ++++++++++++++++++++------ 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 23283981..8f3ae884 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -1,16 +1,27 @@ +from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from account.serializers.common import UserShortSerializer from establishment import models from establishment import serializers as model_serializers +from establishment.models import ContactPhone +from gallery.models import Image +from location.models import Address from location.serializers import AddressDetailSerializer, TranslatedField from main.models import Currency -from location.models import Address from main.serializers import AwardSerializer from utils.decorators import with_base_attributes from utils.serializers import TimeZoneChoiceField -from gallery.models import Image -from django.utils.translation import gettext_lazy as _ -from account.serializers.common import UserShortSerializer + + +def phones_handler(phones_list, establishment): + """ + create or update phones for establishment 35016 string + """ + ContactPhone.objects.filter(establishment=establishment).delete() + + for new_phone in phones_list: + ContactPhone.objects.create(establishment=establishment, phone=new_phone) class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSerializer): @@ -37,6 +48,11 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria address_id = serializers.PrimaryKeyRelatedField(write_only=True, source='address', queryset=Address.objects.all()) tz = TimeZoneChoiceField() + phones_list = serializers.ListField( + child=serializers.CharField(max_length=20), + allow_empty=True, + write_only=True, + ) class Meta: model = models.Establishment @@ -62,8 +78,15 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria 'tags', 'tz', 'address_id', + 'phones_list', ] + def create(self, validated_data): + phones_list = validated_data.pop('phones_list') + instance = super().create(validated_data) + phones_handler(phones_list, instance) + return instance + class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): """Establishment create serializer""" @@ -101,6 +124,12 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): 'tags', ] + def update(self, instance, validated_data): + phones_list = validated_data.pop('phones_list') + instance = super().update(instance, validated_data) + phones_handler(phones_list, instance) + return instance + class SocialChoiceSerializers(serializers.ModelSerializer): """SocialChoice serializers.""" @@ -166,7 +195,6 @@ class ContactEmailBackSerializers(model_serializers.PlateSerializer): ] - class PositionBackSerializer(serializers.ModelSerializer): """Position Back serializer.""" @@ -181,6 +209,7 @@ class PositionBackSerializer(serializers.ModelSerializer): 'index_name', ] + # TODO: test decorator @with_base_attributes class EmployeeBackSerializers(serializers.ModelSerializer): @@ -190,24 +219,22 @@ class EmployeeBackSerializers(serializers.ModelSerializer): establishment = serializers.SerializerMethodField() awards = AwardSerializer(many=True, read_only=True) - def get_public_mark(self, obj): """Get last list actual public_mark""" - qs = obj.establishmentemployee_set.actual().order_by('-from_date')\ + qs = obj.establishmentemployee_set.actual().order_by('-from_date') \ .values('establishment__public_mark').first() return qs['establishment__public_mark'] if qs else None - def get_positions(self, obj): """Get last list actual positions""" - est_id = obj.establishmentemployee_set.actual().\ + est_id = obj.establishmentemployee_set.actual(). \ order_by('-from_date').first() if not est_id: return None - qs = obj.establishmentemployee_set.actual()\ - .filter(establishment_id=est_id.establishment_id)\ + qs = obj.establishmentemployee_set.actual() \ + .filter(establishment_id=est_id.establishment_id) \ .prefetch_related('position').values('position') positions = models.Position.objects.filter(id__in=[q['position'] for q in qs]) @@ -216,7 +243,7 @@ class EmployeeBackSerializers(serializers.ModelSerializer): def get_establishment(self, obj): """Get last actual establishment""" - est = obj.establishmentemployee_set.actual().order_by('-from_date')\ + est = obj.establishmentemployee_set.actual().order_by('-from_date') \ .first() if not est: @@ -380,6 +407,7 @@ class EstablishmentNoteListCreateSerializer(EstablishmentNoteBaseSerializer): class EstablishmentAdminListSerializer(UserShortSerializer): """Establishment admin serializer.""" + class Meta: model = UserShortSerializer.Meta.model fields = [ From 35d9f37fab34e62431e59b05594aee8ac249e3e9 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 13 Jan 2020 15:47:50 +0300 Subject: [PATCH 005/119] fix serializer --- apps/establishment/serializers/back.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 8f3ae884..54f46780 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -103,6 +103,11 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): socials = model_serializers.SocialNetworkRelatedSerializers(read_only=False, many=True, ) type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type') + phones_list = serializers.ListField( + child=serializers.CharField(max_length=20), + allow_empty=True, + write_only=True, + ) class Meta: model = models.Establishment @@ -122,6 +127,7 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): 'is_publish', 'address', 'tags', + 'phones_list', ] def update(self, instance, validated_data): From da1fa9d83a001b7c793b653ea1838c547fd1b42d Mon Sep 17 00:00:00 2001 From: dormantman Date: Mon, 13 Jan 2020 18:59:01 +0300 Subject: [PATCH 006/119] Added agenda creating --- apps/news/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 77499b84..3e337de0 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -180,7 +180,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): """News back office base serializer.""" is_published = serializers.BooleanField(source='is_publish', read_only=True) descriptions = serializers.ListField(required=False) - agenda = serializers.RelatedField(many=True, queryset=models.Agenda.objects.all(), required=False) + agenda = AgendaSerializer() class Meta(NewsBaseSerializer.Meta): """Meta class.""" From 11b55e9f4c90934f49baef498937e055d0c2da9c Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Tue, 14 Jan 2020 15:40:49 +0300 Subject: [PATCH 007/119] change chosen tag functional --- apps/establishment/models.py | 2 -- apps/establishment/serializers/common.py | 4 +-- apps/main/models.py | 9 ++++- apps/main/serializers.py | 8 +++-- apps/news/models.py | 1 - apps/tag/serializers.py | 46 +++++++----------------- 6 files changed, 28 insertions(+), 42 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index f5c2d616..c10be29d 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -58,8 +58,6 @@ class EstablishmentType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBas blank=True, null=True, default=None, verbose_name='default image') - chosen_tags = generic.GenericRelation(to='tag.ChosenTag') - class Meta: """Meta class.""" diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index ada87016..d53fca1e 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -8,6 +8,8 @@ from comment.serializers import common as comment_serializers from establishment import models from location.serializers import AddressBaseSerializer, CitySerializer, AddressDetailSerializer, \ CityShortSerializer +from location.serializers import EstablishmentWineRegionBaseSerializer, \ + EstablishmentWineOriginBaseSerializer from main.serializers import AwardSerializer, CurrencySerializer from review.serializers import ReviewShortSerializer from tag.serializers import TagBaseSerializer @@ -16,8 +18,6 @@ from utils import exceptions as utils_exceptions from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer from utils.serializers import (ProjectModelSerializer, TranslatedField, FavoritesCreateSerializer) -from location.serializers import EstablishmentWineRegionBaseSerializer, \ - EstablishmentWineOriginBaseSerializer class ContactPhonesSerializer(serializers.ModelSerializer): diff --git a/apps/main/models.py b/apps/main/models.py index 63a65062..ab06e4f9 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -6,7 +6,7 @@ from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import JSONField from django.core.validators import EMPTY_VALUES -from django.db import connections, connection +from django.db import connections from django.db import models from django.db.models import Q from django.utils.translation import gettext_lazy as _ @@ -16,6 +16,7 @@ from configuration.models import TranslationSettings from location.models import Country from main import methods from review.models import Review +from tag.models import Tag from utils.exceptions import UnprocessableEntityError from utils.methods import dictfetchall from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, @@ -117,6 +118,8 @@ class Feature(ProjectBaseMixin, PlatformMixin): site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature') old_id = models.IntegerField(null=True, blank=True) + chosen_tags = generic.GenericRelation(to='tag.ChosenTag') + class Meta: """Meta class.""" verbose_name = _('Feature') @@ -125,6 +128,10 @@ class Feature(ProjectBaseMixin, PlatformMixin): def __str__(self): return f'{self.slug}' + @property + def get_chosen_tags(self): + return Tag.objects.filter(chosen_tags__in=self.chosen_tags.all()).distinct() + class SiteFeatureQuerySet(models.QuerySet): """Extended queryset for SiteFeature model.""" diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 4b41eae4..6def6300 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -2,11 +2,12 @@ from django.contrib.contenttypes.models import ContentType from rest_framework import serializers +from account.models import User +from account.serializers.back import BackUserSerializer from location.serializers import CountrySerializer from main import models +from tag.serializers import TagBackOfficeSerializer from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer -from account.serializers.back import BackUserSerializer -from account.models import User class FeatureSerializer(serializers.ModelSerializer): @@ -90,6 +91,8 @@ class SiteFeatureSerializer(serializers.ModelSerializer): route = serializers.CharField(source='feature.route.name') source = serializers.IntegerField(source='feature.source') nested = RecursiveFieldSerializer(many=True, allow_null=True) + chosen_tags = TagBackOfficeSerializer( + source='feature.get_chosen_tags', many=True, read_only=True) class Meta: """Meta class.""" @@ -101,6 +104,7 @@ class SiteFeatureSerializer(serializers.ModelSerializer): 'route', 'source', 'nested', + 'chosen_tags', ) diff --git a/apps/news/models.py b/apps/news/models.py index 3beb0d80..79e2bbd0 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -54,7 +54,6 @@ class NewsType(models.Model): name = models.CharField(_('name'), max_length=250) tag_categories = models.ManyToManyField('tag.TagCategory', related_name='news_types') - chosen_tags = generic.GenericRelation(to='tag.ChosenTag') class Meta: """Meta class.""" diff --git a/apps/tag/serializers.py b/apps/tag/serializers.py index 06da4207..61ed0352 100644 --- a/apps/tag/serializers.py +++ b/apps/tag/serializers.py @@ -9,6 +9,7 @@ from tag import models from utils.exceptions import BindingObjectNotFound, ObjectAlreadyAdded, RemovedBindingObjectNotFound from utils.serializers import TranslatedField from utils.models import get_default_locale, get_language, to_locale +from main.models import Feature def translate_obj(obj): @@ -301,48 +302,25 @@ class ChosenTagSerializer(serializers.ModelSerializer): class ChosenTagBindObjectSerializer(serializers.Serializer): """Serializer for binding chosen tag and objects""" - ESTABLISHMENT_TYPE = 'establishment_type' - NEWS_TYPE = 'news_type' - - TYPE_CHOICES = ( - (ESTABLISHMENT_TYPE, 'Establishment type'), - (NEWS_TYPE, 'News type'), - ) - - type = serializers.ChoiceField(TYPE_CHOICES) - object_id = serializers.IntegerField() + feature_id = serializers.IntegerField() def validate(self, attrs): view = self.context.get('view') request = self.context.get('request') - obj_type = attrs.get('type') - obj_id = attrs.get('object_id') + obj_id = attrs.get('feature_id') tag = view.get_object() attrs['tag'] = tag - if obj_type == self.ESTABLISHMENT_TYPE: - establishment_type = EstablishmentType.objects.filter(pk=obj_id). \ - first() - if not establishment_type: - raise BindingObjectNotFound() - if request.method == 'DELETE' and not establishment_type. \ - chosen_tags.filter(tag=tag). \ - exists(): - raise RemovedBindingObjectNotFound() - attrs['related_object'] = establishment_type - - elif obj_type == self.NEWS_TYPE: - news_type = NewsType.objects.filter(pk=obj_id).first() - if not news_type: - raise BindingObjectNotFound() - if request.method == 'POST' and news_type.chosen_tags. \ - filter(tag=tag).exists(): - raise ObjectAlreadyAdded() - if request.method == 'DELETE' and not news_type.chosen_tags. \ - filter(tag=tag).exists(): - raise RemovedBindingObjectNotFound() - attrs['related_object'] = news_type + feature = Feature.objects.filter(pk=obj_id). \ + first() + if not feature: + raise BindingObjectNotFound() + if request.method == 'DELETE' and not feature. \ + chosen_tags.filter(tag=tag). \ + exists(): + raise RemovedBindingObjectNotFound() + attrs['related_object'] = feature return attrs From 5e5d2832cdacc352a49f08c22834d7d2f49024c6 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Tue, 14 Jan 2020 13:39:25 +0000 Subject: [PATCH 008/119] fix sort by product type --- apps/product/models.py | 7 +++++++ apps/product/urls/common.py | 15 +++++++++------ apps/product/views/common.py | 13 ++++++++++--- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/apps/product/models.py b/apps/product/models.py index b8195fa8..d31e2477 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -38,6 +38,13 @@ class ProductType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin (SOUVENIR, 'souvenir'), (BOOK, 'book') ) + + INDEX_PLURAL_ONE = { + 'food': 'food', + 'wines': 'wine', + 'liquors': 'liquor', + } + name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Name'), help_text='{"en-GB":"some text"}') index_name = models.CharField(max_length=50, unique=True, db_index=True, diff --git a/apps/product/urls/common.py b/apps/product/urls/common.py index 4d64b93e..fe0fcd66 100644 --- a/apps/product/urls/common.py +++ b/apps/product/urls/common.py @@ -19,10 +19,13 @@ urlpatterns = [ # similar products by type/subtype # temporary uses single mechanism, bec. description in process - path('slug//similar/wines/', views.SimilarListView.as_view(), - name='similar-wine'), - path('slug//similar/liquors/', views.SimilarListView.as_view(), - name='similar-liquor'), - path('slug//similar/food/', views.SimilarListView.as_view(), - name='similar-food'), + # path('slug//similar/wines/', views.SimilarListView.as_view(), + # name='similar-wine'), + # path('slug//similar/liquors/', views.SimilarListView.as_view(), + # name='similar-liquor'), + # path('slug//similar/food/', views.SimilarListView.as_view(), + # name='similar-food'), + + path('slug//similar//', views.SimilarListView.as_view(), + name='similar-products') ] diff --git a/apps/product/views/common.py b/apps/product/views/common.py index 34990cc3..e723b3de 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -6,7 +6,7 @@ from rest_framework import generics, permissions from comment.models import Comment from comment.serializers import CommentRUDSerializer from product import filters, serializers -from product.models import Product +from product.models import Product, ProductType from utils.views import FavoritesCreateDestroyMixinView from utils.pagination import PortionPagination from django.conf import settings @@ -44,8 +44,15 @@ class ProductSimilarView(ProductListView): """ Return base product instance for a getting list of similar products. """ - product = get_object_or_404(Product.objects.all(), - slug=self.kwargs.get('slug')) + if isinstance(self.kwargs.get('type'), str): + if not self.kwargs.get('type') in ProductType.INDEX_PLURAL_ONE: + return None + product_type = get_object_or_404(ProductType.objects.all(), index_name=ProductType.INDEX_PLURAL_ONE[self.kwargs.get('type')]) + product = get_object_or_404(Product.objects.all(), + slug=self.kwargs.get('slug'), product_type=product_type) + else: + product = get_object_or_404(Product.objects.all(), + slug=self.kwargs.get('slug')) return product From 9edd3fef182b985c764f61be4eb5557b0d2cde48 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Tue, 14 Jan 2020 13:48:55 +0000 Subject: [PATCH 009/119] cleanup code --- apps/product/views/common.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/product/views/common.py b/apps/product/views/common.py index e723b3de..e384a248 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -44,15 +44,16 @@ class ProductSimilarView(ProductListView): """ Return base product instance for a getting list of similar products. """ + find_by = { + 'slug': self.kwargs.get('slug'), + } + if isinstance(self.kwargs.get('type'), str): if not self.kwargs.get('type') in ProductType.INDEX_PLURAL_ONE: return None - product_type = get_object_or_404(ProductType.objects.all(), index_name=ProductType.INDEX_PLURAL_ONE[self.kwargs.get('type')]) - product = get_object_or_404(Product.objects.all(), - slug=self.kwargs.get('slug'), product_type=product_type) - else: - product = get_object_or_404(Product.objects.all(), - slug=self.kwargs.get('slug')) + find_by['product_type'] = get_object_or_404(ProductType.objects.all(), index_name=ProductType.INDEX_PLURAL_ONE[self.kwargs.get('type')]) + + product = get_object_or_404(Product.objects.all(), **find_by) return product @@ -117,6 +118,6 @@ class SimilarListView(ProductSimilarView): base_product = self.get_base_object() if base_product: - return qs.has_location().similar(base_product)[:settings.QUERY_OUTPUT_OBJECTS] + return qs.similar(base_product)[:settings.QUERY_OUTPUT_OBJECTS] else: return qs.none() From 7c9d9e8ed199c7be20b6faccf58ff3e4aca8b78a Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Tue, 14 Jan 2020 13:51:01 +0000 Subject: [PATCH 010/119] filter by location --- apps/product/views/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/product/views/common.py b/apps/product/views/common.py index e384a248..8b0567b2 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -118,6 +118,6 @@ class SimilarListView(ProductSimilarView): base_product = self.get_base_object() if base_product: - return qs.similar(base_product)[:settings.QUERY_OUTPUT_OBJECTS] + return qs.has_location().similar(base_product)[:settings.QUERY_OUTPUT_OBJECTS] else: return qs.none() From 0672bcaef99385b5694ffafbd3d18cc09d13b0c3 Mon Sep 17 00:00:00 2001 From: anton Date: Thu, 9 Jan 2020 11:07:03 +0300 Subject: [PATCH 011/119] command for optimize news images --- .../commands/news_optimize_images.py | 39 +++++++++++++++++++ apps/utils/methods.py | 6 +++ 2 files changed, 45 insertions(+) create mode 100644 apps/news/management/commands/news_optimize_images.py diff --git a/apps/news/management/commands/news_optimize_images.py b/apps/news/management/commands/news_optimize_images.py new file mode 100644 index 00000000..8d5d39d3 --- /dev/null +++ b/apps/news/management/commands/news_optimize_images.py @@ -0,0 +1,39 @@ +# coding=utf-8 +from django.core.management.base import BaseCommand +from django.conf import settings + +from utils.methods import get_url_images_in_text +from news.models import News +from sorl.thumbnail import get_thumbnail + +class Command(BaseCommand): + SORL_THUMBNAIL_ALIASES = 'news_image' + + def add_arguments(self, parser): + parser.add_argument( + '-a', + '--alias', + default=self.SORL_THUMBNAIL_ALIASES, + help='Ключ для параметров оптимизации', + ) + + def optimize(self, text, alias): + for image in get_url_images_in_text(text): + optimized_image = get_thumbnail( + file_=image, + **settings.SORL_THUMBNAIL_ALIASES[alias] + ).url + text = text.replace(image, optimized_image) + self.stdout.write(self.style.SUCCESS(f'Optimized {image} -> {optimized_image}...\n')) + + return text + + def handle(self, *args, **options): + for news in News.objects.all(): + if not isinstance(news.description, dict): + continue + news.description = { + locale: self.optimize(text, options['alias']) + for locale, text in news.description.items() + } + news.save() \ No newline at end of file diff --git a/apps/utils/methods.py b/apps/utils/methods.py index e06881e5..87d174f6 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -169,3 +169,9 @@ def section_name_into_index_name(section_name: str): result = re.findall(re_exp, section_name) if result: return f"{' '.join([word.capitalize() if i == 0 else word for i, word in enumerate(result[:-2])])}" + + +def get_url_images_in_text(text): + """Find images urls in text""" + import re + return re.findall(r'(?:http:|https:)?//.*\.(?:png|jpg|svg)', text) \ No newline at end of file From e70e1d0a67983db2fd19d11dd69f84a80735b423 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Thu, 9 Jan 2020 18:36:28 +0300 Subject: [PATCH 012/119] thumbnail alias for images in news description --- apps/news/management/commands/news_optimize_images.py | 2 +- apps/utils/methods.py | 1 - project/settings/base.py | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/news/management/commands/news_optimize_images.py b/apps/news/management/commands/news_optimize_images.py index 8d5d39d3..b593ba63 100644 --- a/apps/news/management/commands/news_optimize_images.py +++ b/apps/news/management/commands/news_optimize_images.py @@ -7,7 +7,7 @@ from news.models import News from sorl.thumbnail import get_thumbnail class Command(BaseCommand): - SORL_THUMBNAIL_ALIASES = 'news_image' + SORL_THUMBNAIL_ALIASES = 'news_description' def add_arguments(self, parser): parser.add_argument( diff --git a/apps/utils/methods.py b/apps/utils/methods.py index 87d174f6..db418962 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -173,5 +173,4 @@ def section_name_into_index_name(section_name: str): def get_url_images_in_text(text): """Find images urls in text""" - import re return re.findall(r'(?:http:|https:)?//.*\.(?:png|jpg|svg)', text) \ No newline at end of file diff --git a/project/settings/base.py b/project/settings/base.py index 08a36609..ea243584 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -385,6 +385,7 @@ THUMBNAIL_QUALITY = 85 THUMBNAIL_DEBUG = False SORL_THUMBNAIL_ALIASES = { 'news_preview': {'geometry_string': '300x260', 'crop': 'center'}, + 'news_description': {'geometry_string': '100x100', 'quality': 50}, 'news_promo_horizontal_web': {'geometry_string': '1900x600', 'crop': 'center'}, 'news_promo_horizontal_mobile': {'geometry_string': '375x260', 'crop': 'center'}, 'news_tile_horizontal_web': {'geometry_string': '300x275', 'crop': 'center'}, From ba4c6901f9e68b802ffd86d788e4d808d77037af Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Fri, 10 Jan 2020 16:36:21 +0300 Subject: [PATCH 013/119] quality calculate --- .../commands/news_optimize_images.py | 43 ++++++++++++++++--- apps/utils/methods.py | 7 ++- project/settings/base.py | 2 +- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/apps/news/management/commands/news_optimize_images.py b/apps/news/management/commands/news_optimize_images.py index b593ba63..f65bb492 100644 --- a/apps/news/management/commands/news_optimize_images.py +++ b/apps/news/management/commands/news_optimize_images.py @@ -2,12 +2,14 @@ from django.core.management.base import BaseCommand from django.conf import settings -from utils.methods import get_url_images_in_text +from utils.methods import get_url_images_in_text, get_page_size_by_url from news.models import News from sorl.thumbnail import get_thumbnail + class Command(BaseCommand): SORL_THUMBNAIL_ALIASES = 'news_description' + IMAGE_MAX_SIZE_IN_BYTES = 1048576 # ~ 1mb def add_arguments(self, parser): parser.add_argument( @@ -16,24 +18,51 @@ class Command(BaseCommand): default=self.SORL_THUMBNAIL_ALIASES, help='Ключ для параметров оптимизации', ) + parser.add_argument( + '-s', + '--size', + default=self.IMAGE_MAX_SIZE_IN_BYTES, + help='Максимальный размер файла в байтах', + type=int + ) - def optimize(self, text, alias): + def optimize(self, text, alias, max_size): + """optimize news images""" for image in get_url_images_in_text(text): + size = get_page_size_by_url(image) + + if size < max_size: + continue + + quality = round(max_size / (size * 0.01)) optimized_image = get_thumbnail( file_=image, - **settings.SORL_THUMBNAIL_ALIASES[alias] + **settings.SORL_THUMBNAIL_ALIASES[alias], + quality=100, + upscale=False ).url text = text.replace(image, optimized_image) - self.stdout.write(self.style.SUCCESS(f'Optimized {image} -> {optimized_image}...\n')) + self.stdout.write(self.style.SUCCESS(f'Optimized {image} -> {optimized_image}\n' + f'Quality [{quality}%]\n')) return text def handle(self, *args, **options): + alias = options['alias'] + size = options['size'] + + if alias not in settings.SORL_THUMBNAIL_ALIASES: + self.stdout.write(self.style.ERROR(f'{alias} not found in alias')) + return + + self.optimize('https://upload.wikimedia.org/wikipedia/commons/b/b9/Pizigani_1367_Chart_1MB.jpg', + 'news_description', size) + for news in News.objects.all(): if not isinstance(news.description, dict): continue news.description = { - locale: self.optimize(text, options['alias']) - for locale, text in news.description.items() + locale: self.optimize(text, alias, size) + for locale, text in news.description.items() } - news.save() \ No newline at end of file + news.save() diff --git a/apps/utils/methods.py b/apps/utils/methods.py index db418962..c4339ab3 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -173,4 +173,9 @@ def section_name_into_index_name(section_name: str): def get_url_images_in_text(text): """Find images urls in text""" - return re.findall(r'(?:http:|https:)?//.*\.(?:png|jpg|svg)', text) \ No newline at end of file + return re.findall(r'(?:http:|https:)?//.*\.(?:png|jpg|svg)', text) + +def get_page_size_by_url(url): + """Get page size by Content-Length header""" + response = requests.head(url) + return int(response.headers.get('content-length')) \ No newline at end of file diff --git a/project/settings/base.py b/project/settings/base.py index ea243584..80e3c881 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -385,7 +385,7 @@ THUMBNAIL_QUALITY = 85 THUMBNAIL_DEBUG = False SORL_THUMBNAIL_ALIASES = { 'news_preview': {'geometry_string': '300x260', 'crop': 'center'}, - 'news_description': {'geometry_string': '100x100', 'quality': 50}, + 'news_description': {'geometry_string': '100x100'}, 'news_promo_horizontal_web': {'geometry_string': '1900x600', 'crop': 'center'}, 'news_promo_horizontal_mobile': {'geometry_string': '375x260', 'crop': 'center'}, 'news_tile_horizontal_web': {'geometry_string': '300x275', 'crop': 'center'}, From 77cca6c6d72eb5081ca7a0a032623ddedf85e8b7 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 10 Jan 2020 17:47:23 +0300 Subject: [PATCH 014/119] resize photos --- .../commands/news_optimize_images.py | 27 ++++++++++--------- apps/utils/methods.py | 12 ++++++--- apps/utils/thumbnail_engine.py | 4 +-- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/apps/news/management/commands/news_optimize_images.py b/apps/news/management/commands/news_optimize_images.py index f65bb492..ee93061a 100644 --- a/apps/news/management/commands/news_optimize_images.py +++ b/apps/news/management/commands/news_optimize_images.py @@ -29,21 +29,22 @@ class Command(BaseCommand): def optimize(self, text, alias, max_size): """optimize news images""" for image in get_url_images_in_text(text): - size = get_page_size_by_url(image) + size, width, height = get_page_size_by_url(image) if size < max_size: continue - quality = round(max_size / (size * 0.01)) + percents = round(max_size / (size * 0.01)) + width = round(width * percents / 100) + height = round(height * percents / 100) optimized_image = get_thumbnail( file_=image, - **settings.SORL_THUMBNAIL_ALIASES[alias], - quality=100, + geometry_string=f'{width}x{height}', upscale=False ).url text = text.replace(image, optimized_image) self.stdout.write(self.style.SUCCESS(f'Optimized {image} -> {optimized_image}\n' - f'Quality [{quality}%]\n')) + f'Quality [{percents}%]\n')) return text @@ -58,11 +59,11 @@ class Command(BaseCommand): self.optimize('https://upload.wikimedia.org/wikipedia/commons/b/b9/Pizigani_1367_Chart_1MB.jpg', 'news_description', size) - for news in News.objects.all(): - if not isinstance(news.description, dict): - continue - news.description = { - locale: self.optimize(text, alias, size) - for locale, text in news.description.items() - } - news.save() + # for news in News.objects.all(): + # if not isinstance(news.description, dict): + # continue + # news.description = { + # locale: self.optimize(text, alias, size) + # for locale, text in news.description.items() + # } + # news.save() diff --git a/apps/utils/methods.py b/apps/utils/methods.py index c4339ab3..095b406f 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -3,6 +3,8 @@ import logging import random import re import string +from io import BytesIO +from PIL import Image from collections import namedtuple import requests @@ -175,7 +177,9 @@ def get_url_images_in_text(text): """Find images urls in text""" return re.findall(r'(?:http:|https:)?//.*\.(?:png|jpg|svg)', text) -def get_page_size_by_url(url): - """Get page size by Content-Length header""" - response = requests.head(url) - return int(response.headers.get('content-length')) \ No newline at end of file +def get_page_size_by_url(url) -> (int, int, int): + """Returns image size (bytes, width, height)""" + image_raw = requests.get(url) + image = Image.open(BytesIO(image_raw.content)) + width, height = image.size + return int(image_raw.headers.get('content-length')), width, height \ No newline at end of file diff --git a/apps/utils/thumbnail_engine.py b/apps/utils/thumbnail_engine.py index f55d58f8..99192716 100644 --- a/apps/utils/thumbnail_engine.py +++ b/apps/utils/thumbnail_engine.py @@ -5,13 +5,11 @@ from sorl.thumbnail.engines.pil_engine import Engine as PILEngine class GMEngine(PILEngine): def create(self, image, geometry, options): - """ - Processing conductor, returns the thumbnail as an image engine instance - """ image = self.cropbox(image, geometry, options) image = self.orientation(image, geometry, options) image = self.colorspace(image, geometry, options) image = self.remove_border(image, options) + image = self.scale(image, geometry, options) image = self.crop(image, geometry, options) image = self.rounded(image, geometry, options) image = self.blur(image, geometry, options) From 710eb274ed24eb98e91d84b671b7e5cab154e61e Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Mon, 13 Jan 2020 10:19:47 +0300 Subject: [PATCH 015/119] remove debug info. Cleanup code. Set default quality --- .../commands/news_optimize_images.py | 50 ++++++++----------- apps/utils/methods.py | 2 +- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/apps/news/management/commands/news_optimize_images.py b/apps/news/management/commands/news_optimize_images.py index ee93061a..7e1de7e1 100644 --- a/apps/news/management/commands/news_optimize_images.py +++ b/apps/news/management/commands/news_optimize_images.py @@ -1,23 +1,16 @@ # coding=utf-8 from django.core.management.base import BaseCommand -from django.conf import settings -from utils.methods import get_url_images_in_text, get_page_size_by_url +from utils.methods import get_url_images_in_text, get_image_meta_by_url from news.models import News from sorl.thumbnail import get_thumbnail class Command(BaseCommand): - SORL_THUMBNAIL_ALIASES = 'news_description' IMAGE_MAX_SIZE_IN_BYTES = 1048576 # ~ 1mb + IMAGE_QUALITY_PERCENTS = 50 def add_arguments(self, parser): - parser.add_argument( - '-a', - '--alias', - default=self.SORL_THUMBNAIL_ALIASES, - help='Ключ для параметров оптимизации', - ) parser.add_argument( '-s', '--size', @@ -25,11 +18,18 @@ class Command(BaseCommand): help='Максимальный размер файла в байтах', type=int ) + parser.add_argument( + '-q', + '--quality', + default=self.IMAGE_QUALITY_PERCENTS, + help='Качество изображения', + type=int + ) - def optimize(self, text, alias, max_size): + def optimize(self, text, max_size, max_quality): """optimize news images""" for image in get_url_images_in_text(text): - size, width, height = get_page_size_by_url(image) + size, width, height = get_image_meta_by_url(image) if size < max_size: continue @@ -40,7 +40,8 @@ class Command(BaseCommand): optimized_image = get_thumbnail( file_=image, geometry_string=f'{width}x{height}', - upscale=False + upscale=False, + quality=max_quality ).url text = text.replace(image, optimized_image) self.stdout.write(self.style.SUCCESS(f'Optimized {image} -> {optimized_image}\n' @@ -49,21 +50,14 @@ class Command(BaseCommand): return text def handle(self, *args, **options): - alias = options['alias'] size = options['size'] + quality = options['quality'] - if alias not in settings.SORL_THUMBNAIL_ALIASES: - self.stdout.write(self.style.ERROR(f'{alias} not found in alias')) - return - - self.optimize('https://upload.wikimedia.org/wikipedia/commons/b/b9/Pizigani_1367_Chart_1MB.jpg', - 'news_description', size) - - # for news in News.objects.all(): - # if not isinstance(news.description, dict): - # continue - # news.description = { - # locale: self.optimize(text, alias, size) - # for locale, text in news.description.items() - # } - # news.save() + for news in News.objects.all(): + if not isinstance(news.description, dict): + continue + news.description = { + locale: self.optimize(text, size, quality) + for locale, text in news.description.items() + } + news.save() diff --git a/apps/utils/methods.py b/apps/utils/methods.py index 095b406f..45f15c58 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -177,7 +177,7 @@ def get_url_images_in_text(text): """Find images urls in text""" return re.findall(r'(?:http:|https:)?//.*\.(?:png|jpg|svg)', text) -def get_page_size_by_url(url) -> (int, int, int): +def get_image_meta_by_url(url) -> (int, int, int, str): """Returns image size (bytes, width, height)""" image_raw = requests.get(url) image = Image.open(BytesIO(image_raw.content)) From 80dec0b6ee8357fc9d00224251dc813f5fd85447 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 14 Jan 2020 17:20:41 +0300 Subject: [PATCH 016/119] compress images with data transfer --- make_data_migration.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/make_data_migration.sh b/make_data_migration.sh index 933639fa..edc50180 100755 --- a/make_data_migration.sh +++ b/make_data_migration.sh @@ -27,4 +27,7 @@ ./manage.py transfer --overlook ./manage.py transfer --inquiries ./manage.py transfer --product_review -./manage.py transfer --transfer_text_review \ No newline at end of file +./manage.py transfer --transfer_text_review + +# оптимизация изображений +/manage.py news_optimize_images # сжимает картинки в описаниях новостей \ No newline at end of file From 3f19699e1d240c75487c27fa9d0562916980ecef Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 14 Jan 2020 17:32:13 +0300 Subject: [PATCH 017/119] supress issues w/ news images compression --- apps/news/management/commands/news_optimize_images.py | 7 ++++++- apps/utils/methods.py | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/news/management/commands/news_optimize_images.py b/apps/news/management/commands/news_optimize_images.py index 7e1de7e1..48553d5e 100644 --- a/apps/news/management/commands/news_optimize_images.py +++ b/apps/news/management/commands/news_optimize_images.py @@ -29,9 +29,14 @@ class Command(BaseCommand): def optimize(self, text, max_size, max_quality): """optimize news images""" for image in get_url_images_in_text(text): - size, width, height = get_image_meta_by_url(image) + try: + size, width, height = get_image_meta_by_url(image) + except IOError as ie: + self.stdout.write(self.style.NOTICE(f'{ie}\n')) + continue if size < max_size: + self.stdout.write(self.style.SUCCESS(f'No need to compress images size is {size / 1024}Mb\n')) continue percents = round(max_size / (size * 0.01)) diff --git a/apps/utils/methods.py b/apps/utils/methods.py index 45f15c58..18575c07 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -121,6 +121,7 @@ def absolute_url_decorator(func): return f'{settings.MEDIA_URL}{url_path}/' else: return url_path + return get_absolute_image_url @@ -177,7 +178,8 @@ def get_url_images_in_text(text): """Find images urls in text""" return re.findall(r'(?:http:|https:)?//.*\.(?:png|jpg|svg)', text) -def get_image_meta_by_url(url) -> (int, int, int, str): + +def get_image_meta_by_url(url) -> (int, int, int): """Returns image size (bytes, width, height)""" image_raw = requests.get(url) image = Image.open(BytesIO(image_raw.content)) From 440a263ea55a30618b2918eca4bed3f304ec3696 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 14 Jan 2020 17:34:33 +0300 Subject: [PATCH 018/119] verbose news optimize command fix --- apps/news/management/commands/news_optimize_images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/news/management/commands/news_optimize_images.py b/apps/news/management/commands/news_optimize_images.py index 48553d5e..440afbc2 100644 --- a/apps/news/management/commands/news_optimize_images.py +++ b/apps/news/management/commands/news_optimize_images.py @@ -36,7 +36,7 @@ class Command(BaseCommand): continue if size < max_size: - self.stdout.write(self.style.SUCCESS(f'No need to compress images size is {size / 1024}Mb\n')) + self.stdout.write(self.style.SUCCESS(f'No need to compress images size is {size / (2**20)}Mb\n')) continue percents = round(max_size / (size * 0.01)) From c110dc3b96790b9a19aa334a62a5b14a6b7374b2 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Tue, 14 Jan 2020 06:23:08 +0000 Subject: [PATCH 019/119] optimize collection images --- .../commands/collection_optimize_images.py | 29 +++++++++++++++++++ .../establishment_optimize_preview_image.py | 28 ++++++++++++++++++ apps/utils/methods.py | 3 +- apps/utils/thumbnail_engine.py | 1 + project/settings/base.py | 2 ++ 5 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 apps/collection/management/commands/collection_optimize_images.py create mode 100644 apps/establishment/management/commands/establishment_optimize_preview_image.py diff --git a/apps/collection/management/commands/collection_optimize_images.py b/apps/collection/management/commands/collection_optimize_images.py new file mode 100644 index 00000000..2b6c9e3d --- /dev/null +++ b/apps/collection/management/commands/collection_optimize_images.py @@ -0,0 +1,29 @@ +from django.conf import settings +from django.core.management.base import BaseCommand +from django.db import transaction +from sorl.thumbnail import get_thumbnail + +from collection.models import Collection +from utils.methods import image_url_valid, get_image_meta_by_url + + +class Command(BaseCommand): + SORL_THUMBNAIL_ALIAS = 'collection_image' + + def handle(self, *args, **options): + with transaction.atomic(): + for collection in Collection.objects.all(): + if not image_url_valid(collection.image_url): + continue + + _, width, height = get_image_meta_by_url(collection.image_url) + sorl_settings = settings.SORL_THUMBNAIL_ALIASES[self.SORL_THUMBNAIL_ALIAS] + sorl_width_height = sorl_settings['geometry_string'].split('x') + + if int(sorl_width_height[0]) > width or int(sorl_width_height[1]) > height: + collection.image_url = get_thumbnail( + file_=collection.image_url, + **settings.SORL_THUMBNAIL_ALIASES[self.SORL_THUMBNAIL_ALIAS] + ).url + + collection.save() diff --git a/apps/establishment/management/commands/establishment_optimize_preview_image.py b/apps/establishment/management/commands/establishment_optimize_preview_image.py new file mode 100644 index 00000000..50a73a26 --- /dev/null +++ b/apps/establishment/management/commands/establishment_optimize_preview_image.py @@ -0,0 +1,28 @@ +from django.conf import settings +from django.core.management.base import BaseCommand +from django.db import transaction +from sorl.thumbnail import get_thumbnail + +from establishment.models import Establishment +from utils.methods import image_url_valid, get_image_meta_by_url + + +class Command(BaseCommand): + SORL_THUMBNAIL_ALIAS = 'establishment_collection_image' + + def handle(self, *args, **options): + with transaction.atomic(): + for establishment in Establishment.objects.all(): + if not image_url_valid(establishment.preview_image_url): + continue + + _, width, height = get_image_meta_by_url(establishment.preview_image_url) + sorl_settings = settings.SORL_THUMBNAIL_ALIASES[self.SORL_THUMBNAIL_ALIAS] + sorl_width_height = sorl_settings['geometry_string'].split('x') + + if int(sorl_width_height[0]) > width or int(sorl_width_height[1]) > height: + establishment.preview_image_url = get_thumbnail( + file_=establishment.preview_image_url, + **sorl_settings + ) + establishment.save() diff --git a/apps/utils/methods.py b/apps/utils/methods.py index 18575c07..9fd26dca 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -3,9 +3,10 @@ import logging import random import re import string +from collections import namedtuple from io import BytesIO from PIL import Image -from collections import namedtuple + import requests from django.conf import settings diff --git a/apps/utils/thumbnail_engine.py b/apps/utils/thumbnail_engine.py index 99192716..8e3b50ba 100644 --- a/apps/utils/thumbnail_engine.py +++ b/apps/utils/thumbnail_engine.py @@ -11,6 +11,7 @@ class GMEngine(PILEngine): image = self.remove_border(image, options) image = self.scale(image, geometry, options) image = self.crop(image, geometry, options) + image = self.scale(image, geometry, options) image = self.rounded(image, geometry, options) image = self.blur(image, geometry, options) image = self.padding(image, geometry, options) diff --git a/project/settings/base.py b/project/settings/base.py index 80e3c881..0b5b9c20 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -412,6 +412,8 @@ SORL_THUMBNAIL_ALIASES = { 'city_detail': {'geometry_string': '1120x1120', 'crop': 'center'}, 'city_original': {'geometry_string': '2048x1536', 'crop': 'center'}, 'type_preview': {'geometry_string': '300x260', 'crop': 'center'}, + 'collection_image': {'geometry_string': '940x620', 'upscale': False, 'quality': 100}, + 'establishment_collection_image': {'geometry_string': '940x620', 'upscale': False, 'quality': 100} } From 49673d64afe230e7582a9eac8bb042a4668333d1 Mon Sep 17 00:00:00 2001 From: dormantman Date: Tue, 14 Jan 2020 18:20:57 +0300 Subject: [PATCH 020/119] Added automatic username creation --- apps/authorization/serializers/common.py | 18 +++++++++++++----- apps/utils/methods.py | 10 +++++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index e6a9bdc7..4ebc1c99 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -18,6 +18,7 @@ from utils.tokens import GMRefreshToken # Serializers class SignupSerializer(serializers.ModelSerializer): """Signup serializer serializer mixin""" + class Meta: model = account_models.User fields = ( @@ -35,11 +36,18 @@ class SignupSerializer(serializers.ModelSerializer): def validate_username(self, value): """Custom username validation""" - valid = utils_methods.username_validator(username=value) - if not valid: - raise utils_exceptions.NotValidUsernameError() - if account_models.User.objects.filter(username__iexact=value).exists(): - raise serializers.ValidationError() + if value: + valid = utils_methods.username_validator(username=value) + if not valid: + raise utils_exceptions.NotValidUsernameError() + if account_models.User.objects.filter(username__iexact=value).exists(): + raise serializers.ValidationError() + + else: + value = utils_methods.username_random() + while account_models.User.objects.filter(username__iexact=value).exists(): + value = utils_methods.username_random() + return value def validate_email(self, value): diff --git a/apps/utils/methods.py b/apps/utils/methods.py index 18575c07..71101a9e 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -55,6 +55,14 @@ def username_validator(username: str) -> bool: return True +def username_random(): + """Generate random username""" + return '{letters}{digits}'.format( + letters=''.join([random.choice(string.ascii_lowercase) for _ in range(5)]), + digits=random.randrange(100, 1000) + ) + + def image_path(instance, filename): """Determine avatar path method.""" filename = '%s.jpeg' % generate_code() @@ -184,4 +192,4 @@ def get_image_meta_by_url(url) -> (int, int, int): image_raw = requests.get(url) image = Image.open(BytesIO(image_raw.content)) width, height = image.size - return int(image_raw.headers.get('content-length')), width, height \ No newline at end of file + return int(image_raw.headers.get('content-length')), width, height From 5c95c4923f044d03fe9ef714643ae72c31c60b45 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 14 Jan 2020 18:40:30 +0300 Subject: [PATCH 021/119] refactored roles, refactored response for User detail view --- apps/account/admin.py | 4 +- .../migrations/0032_auto_20200114_1311.py | 26 +++++++ apps/account/models.py | 15 +++- apps/account/serializers/__init__.py | 3 + apps/account/serializers/back.py | 13 +--- apps/account/serializers/common.py | 20 +++++- apps/account/urls/back.py | 6 +- apps/account/views/back.py | 9 +-- apps/main/admin.py | 19 ++++++ .../migrations/0046_auto_20200114_1218.py | 44 ++++++++++++ apps/main/models.py | 39 ++++++++++- apps/main/serializers.py | 68 ++++++++++++++----- 12 files changed, 225 insertions(+), 41 deletions(-) create mode 100644 apps/account/migrations/0032_auto_20200114_1311.py create mode 100644 apps/main/migrations/0046_auto_20200114_1218.py diff --git a/apps/account/admin.py b/apps/account/admin.py index 0a6de6a0..04923cc9 100644 --- a/apps/account/admin.py +++ b/apps/account/admin.py @@ -8,11 +8,13 @@ from account import models @admin.register(models.Role) class RoleAdmin(admin.ModelAdmin): list_display = ['role', 'country'] + raw_id_fields = ['country', ] @admin.register(models.UserRole) class UserRoleAdmin(admin.ModelAdmin): - list_display = ['user', 'role', 'establishment'] + list_display = ['user', 'role', 'establishment', ] + raw_id_fields = ['user', 'role', 'establishment', 'requester', ] @admin.register(models.User) diff --git a/apps/account/migrations/0032_auto_20200114_1311.py b/apps/account/migrations/0032_auto_20200114_1311.py new file mode 100644 index 00000000..8e0412a3 --- /dev/null +++ b/apps/account/migrations/0032_auto_20200114_1311.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.7 on 2020-01-14 13:11 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0046_auto_20200114_1218'), + ('account', '0031_user_last_country'), + ] + + operations = [ + migrations.AddField( + model_name='role', + name='navigation_bar_permission', + field=models.ForeignKey(blank=True, help_text='navigation bar item permission', null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.NavigationBarPermission', verbose_name='navigation bar permission'), + ), + migrations.AlterField( + model_name='userrole', + name='requester', + field=models.ForeignKey(blank=True, default=None, help_text='A user (REQUESTER) who requests a role change for a USER', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='roles_requested', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/apps/account/models.py b/apps/account/models.py index 5bf66321..1f050cdb 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -53,6 +53,7 @@ class Role(ProjectBaseMixin): (LIQUOR_REVIEWER, 'Liquor reviewer'), (PRODUCT_REVIEWER, 'Product reviewer'), ) + role = models.PositiveIntegerField(verbose_name=_('Role'), choices=ROLE_CHOICES, null=False, blank=False) country = models.ForeignKey(Country, verbose_name=_('Country'), @@ -62,6 +63,11 @@ class Role(ProjectBaseMixin): establishment_subtype = models.ForeignKey(EstablishmentSubType, verbose_name=_('Establishment subtype'), null=True, blank=True, on_delete=models.SET_NULL) + navigation_bar_permission = models.ForeignKey('main.NavigationBarPermission', + blank=True, null=True, + on_delete=models.SET_NULL, + help_text='navigation bar item permission', + verbose_name=_('navigation bar permission')) class UserManager(BaseUserManager): @@ -348,6 +354,7 @@ class UserRole(ProjectBaseMixin): (CANCELLED, _('cancelled')), (REJECTED, _('rejected')) ) + user = models.ForeignKey( 'account.User', verbose_name=_('User'), on_delete=models.CASCADE) role = models.ForeignKey( @@ -358,9 +365,11 @@ class UserRole(ProjectBaseMixin): state = models.CharField( _('state'), choices=STATE_CHOICES, max_length=10, default=PENDING) - requester = models.ForeignKey( - 'account.User', blank=True, null=True, default=None, related_name='roles_requested', - on_delete=models.SET_NULL) + requester = models.ForeignKey('account.User', on_delete=models.SET_NULL, + blank=True, null=True, default=None, + related_name='roles_requested', + help_text='A user (REQUESTER) who requests a ' + 'role change for a USER') class Meta: unique_together = ['user', 'role', 'establishment', 'state'] diff --git a/apps/account/serializers/__init__.py b/apps/account/serializers/__init__.py index e69de29b..b0ba735d 100644 --- a/apps/account/serializers/__init__.py +++ b/apps/account/serializers/__init__.py @@ -0,0 +1,3 @@ +from account.serializers.common import * +from account.serializers.web import * +from account.serializers.back import * diff --git a/apps/account/serializers/back.py b/apps/account/serializers/back.py index 4c810ee7..4b9787d1 100644 --- a/apps/account/serializers/back.py +++ b/apps/account/serializers/back.py @@ -3,15 +3,7 @@ from rest_framework import serializers from account import models from account.models import User from main.models import SiteSettings - - -class RoleSerializer(serializers.ModelSerializer): - class Meta: - model = models.Role - fields = [ - 'role', - 'country' - ] +# from account.serializers.common import RoleBaseSerializer class _SiteSettingsSerializer(serializers.ModelSerializer): @@ -26,6 +18,7 @@ class _SiteSettingsSerializer(serializers.ModelSerializer): class BackUserSerializer(serializers.ModelSerializer): last_country = _SiteSettingsSerializer(read_only=True) + # roles = RoleBaseSerializer(many=True, read_only=True) class Meta: model = User @@ -45,12 +38,12 @@ class BackUserSerializer(serializers.ModelSerializer): 'unconfirmed_email', 'email_confirmed', 'newsletter', - 'roles', 'password', 'city', 'locale', 'last_ip', 'last_country', + # 'roles', ) extra_kwargs = { 'password': {'write_only': True}, diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index bee3f7ac..a3347b46 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -1,7 +1,7 @@ """Common account serializers""" from django.conf import settings -from django.utils.translation import gettext_lazy as _ from django.contrib.auth import password_validation as password_validators +from django.utils.translation import gettext_lazy as _ from fcm_django.models import FCMDevice from rest_framework import exceptions from rest_framework import serializers @@ -11,9 +11,25 @@ from account import models, tasks from notification.models import Subscriber from utils import exceptions as utils_exceptions from utils import methods as utils_methods +from main.serializers import NavigationBarPermissionBaseSerializer # User serializers +class RoleBaseSerializer(serializers.ModelSerializer): + """Serializer for model Role.""" + role_display = serializers.CharField(source='get_role_display', read_only=True) + navigation_bar_permission = NavigationBarPermissionBaseSerializer(read_only=True) + + class Meta: + """Meta class.""" + model = models.Role + fields = [ + 'id', + 'role_display', + 'navigation_bar_permission', + ] + + class UserSerializer(serializers.ModelSerializer): """User serializer.""" # RESPONSE @@ -26,6 +42,7 @@ class UserSerializer(serializers.ModelSerializer): email = serializers.EmailField( validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),), required=False) + roles = RoleBaseSerializer(many=True, read_only=True) class Meta: model = models.User @@ -39,6 +56,7 @@ class UserSerializer(serializers.ModelSerializer): 'email', 'email_confirmed', 'newsletter', + 'roles', ] extra_kwargs = { 'first_name': {'required': False, 'write_only': True, }, diff --git a/apps/account/urls/back.py b/apps/account/urls/back.py index a46b39bf..b76e8110 100644 --- a/apps/account/urls/back.py +++ b/apps/account/urls/back.py @@ -6,9 +6,9 @@ from account.views import back as views app_name = 'account' urlpatterns = [ - path('role/', views.RoleLstView.as_view(), name='role-list-create'), - path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'), - path('user/', views.UserLstView.as_view(), name='user-create-list'), + path('role/', views.RoleListView.as_view(), name='role-list-create'), + path('user-role/', views.UserRoleListView.as_view(), name='user-role-list-create'), + path('user/', views.UserListView.as_view(), name='user-create-list'), path('user//', views.UserRUDView.as_view(), name='user-rud'), path('user//csv/', views.get_user_csv, name='user-csv'), ] diff --git a/apps/account/views/back.py b/apps/account/views/back.py index ded254dc..cb68cec6 100644 --- a/apps/account/views/back.py +++ b/apps/account/views/back.py @@ -8,19 +8,20 @@ from rest_framework.authtoken.models import Token from account import models from account.models import User from account.serializers import back as serializers +from account.serializers.common import RoleBaseSerializer -class RoleLstView(generics.ListCreateAPIView): - serializer_class = serializers.RoleSerializer +class RoleListView(generics.ListCreateAPIView): + serializer_class = RoleBaseSerializer queryset = models.Role.objects.all() -class UserRoleLstView(generics.ListCreateAPIView): +class UserRoleListView(generics.ListCreateAPIView): serializer_class = serializers.UserRoleSerializer queryset = models.UserRole.objects.all() -class UserLstView(generics.ListCreateAPIView): +class UserListView(generics.ListCreateAPIView): """User list create view.""" queryset = User.objects.prefetch_related('roles') serializer_class = serializers.BackUserSerializer diff --git a/apps/main/admin.py b/apps/main/admin.py index d7f702e9..1cd6a774 100644 --- a/apps/main/admin.py +++ b/apps/main/admin.py @@ -14,6 +14,14 @@ class SiteSettingsAdmin(admin.ModelAdmin): inlines = [SiteSettingsInline, ] +@admin.register(models.SiteFeature) +class SiteFeatureAdmin(admin.ModelAdmin): + """Site feature admin conf.""" + list_display = ['id', 'site_settings', 'feature', + 'published', 'main', 'backoffice', ] + raw_id_fields = ['site_settings', 'feature', ] + + @admin.register(models.Feature) class FeatureAdmin(admin.ModelAdmin): """Feature admin conf.""" @@ -80,3 +88,14 @@ class PanelAdmin(admin.ModelAdmin): list_display = ('id', 'name', 'user', 'created',) raw_id_fields = ('user',) list_display_links = ('id', 'name',) + + +@admin.register(models.NavigationBarPermission) +class NavigationBarPermissionAdmin(admin.ModelAdmin): + """NavigationBarPermission admin.""" + list_display = ('section', 'permission_mode_display', ) + raw_id_fields = ('section', ) + + def permission_mode_display(self, obj): + """Get permission mode display.""" + return obj.get_permission_mode_display() diff --git a/apps/main/migrations/0046_auto_20200114_1218.py b/apps/main/migrations/0046_auto_20200114_1218.py new file mode 100644 index 00000000..9d97fb2c --- /dev/null +++ b/apps/main/migrations/0046_auto_20200114_1218.py @@ -0,0 +1,44 @@ +# Generated by Django 2.2.7 on 2020-01-14 12:18 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0045_carousel_is_international'), + ] + + operations = [ + migrations.AddField( + model_name='sitefeature', + name='backoffice', + field=models.BooleanField(default=False, help_text='shows on backoffice page', verbose_name='backoffice'), + ), + migrations.AlterField( + model_name='sitefeature', + name='main', + field=models.BooleanField(default=False, help_text='shows on main page', verbose_name='Main'), + ), + migrations.AlterField( + model_name='sitefeature', + name='nested', + field=models.ManyToManyField(to='main.SiteFeature', blank=True), + ), + migrations.CreateModel( + name='NavigationBarPermission', + 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')), + ('permission_mode', models.PositiveSmallIntegerField(choices=[(0, 'read'), (1, 'write')], default=0, help_text='READ - allows only retrieve data,WRITE - allows to perform any operations over the object', verbose_name='permission mode')), + ('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.SiteFeature', verbose_name='section')), + ], + options={ + 'verbose_name': 'Navigation bar item permission', + 'verbose_name_plural': 'Navigation bar item permissions', + }, + ), + ] diff --git a/apps/main/models.py b/apps/main/models.py index 63a65062..b9433b7d 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -6,10 +6,11 @@ from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import JSONField from django.core.validators import EMPTY_VALUES -from django.db import connections, connection +from django.db import connections from django.db import models from django.db.models import Q from django.utils.translation import gettext_lazy as _ +from mptt.models import MPTTModel, TreeForeignKey from rest_framework import exceptions from configuration.models import TranslationSettings @@ -144,9 +145,15 @@ class SiteFeature(ProjectBaseMixin): site_settings = models.ForeignKey(SiteSettings, on_delete=models.CASCADE) feature = models.ForeignKey(Feature, on_delete=models.PROTECT) + published = models.BooleanField(default=False, verbose_name=_('Published')) - main = models.BooleanField(default=False, verbose_name=_('Main')) - nested = models.ManyToManyField('self', symmetrical=False) + main = models.BooleanField(default=False, + help_text='shows on main page', + verbose_name=_('Main'),) + backoffice = models.BooleanField(default=False, + help_text='shows on backoffice page', + verbose_name=_('backoffice'),) + nested = models.ManyToManyField('self', blank=True, symmetrical=False) old_id = models.IntegerField(null=True, blank=True) objects = SiteFeatureQuerySet.as_manager() @@ -520,3 +527,29 @@ class Panel(ProjectBaseMixin): params = params + new_params query = self.query + limit_offset return query, params + + +class NavigationBarPermission(ProjectBaseMixin): + """Model for navigation bar item permissions.""" + READ = 0 + WRITE = 1 + + PERMISSION_MODES = ( + (READ, _('read')), + (WRITE, _('write')), + ) + + section = models.ForeignKey('main.SiteFeature', + on_delete=models.CASCADE, + verbose_name=_('section')) + permission_mode = models.PositiveSmallIntegerField(choices=PERMISSION_MODES, + default=READ, + help_text='READ - allows only retrieve data,' + 'WRITE - allows to perform any ' + 'operations over the object', + verbose_name=_('permission mode')) + + class Meta: + """Meta class.""" + verbose_name = _('Navigation bar item permission') + verbose_name_plural = _('Navigation bar item permissions') diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 4b41eae4..4f097273 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -2,11 +2,11 @@ from django.contrib.contenttypes.models import ContentType from rest_framework import serializers +from account.models import User +from account.serializers.back import BackUserSerializer from location.serializers import CountrySerializer from main import models from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer -from account.serializers.back import BackUserSerializer -from account.models import User class FeatureSerializer(serializers.ModelSerializer): @@ -84,24 +84,43 @@ class FooterBackSerializer(FooterSerializer): class SiteFeatureSerializer(serializers.ModelSerializer): - id = serializers.IntegerField(source='feature.id') - slug = serializers.CharField(source='feature.slug') - priority = serializers.IntegerField(source='feature.priority') - route = serializers.CharField(source='feature.route.name') - source = serializers.IntegerField(source='feature.source') - nested = RecursiveFieldSerializer(many=True, allow_null=True) + id = serializers.IntegerField(source='feature.id', allow_null=True) + slug = serializers.CharField(source='feature.slug', allow_null=True) + priority = serializers.IntegerField(source='feature.priority', allow_null=True) + route = serializers.CharField(source='feature.route.name', allow_null=True) + source = serializers.IntegerField(source='feature.source', allow_null=True) + nested = RecursiveFieldSerializer(many=True, read_only=True, allow_null=True) class Meta: """Meta class.""" model = models.SiteFeature - fields = ('main', - 'id', - 'slug', - 'priority', - 'route', - 'source', - 'nested', - ) + fields = ( + 'id', + 'main', + 'slug', + 'priority', + 'route', + 'source', + 'nested', + ) + + +class NavigationBarSectionBaseSerializer(SiteFeatureSerializer): + """Serializer for navigation bar.""" + source_display = serializers.CharField(source='feature.get_source_display', + read_only=True) + + class Meta(SiteFeatureSerializer.Meta): + model = models.SiteFeature + fields = [ + 'id', + 'slug', + 'route', + 'source', + 'source_display', + 'priority', + 'nested', + ] class SiteSettingsSerializer(serializers.ModelSerializer): @@ -331,3 +350,20 @@ class PanelExecuteSerializer(serializers.ModelSerializer): 'user', 'user_id' ] + + +class NavigationBarPermissionBaseSerializer(serializers.ModelSerializer): + """Navigation bar permission serializer.""" + + section = NavigationBarSectionBaseSerializer(read_only=True) + permission_mode_display = serializers.CharField(source='get_permission_mode_display', + read_only=True) + + class Meta: + """Meta class.""" + model = models.NavigationBarPermission + fields = [ + 'id', + 'section', + 'permission_mode_display', + ] From da9f393a5290962cb77b8e10b73fa24c9ed88292 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 14 Jan 2020 21:28:23 +0300 Subject: [PATCH 022/119] try to fix issue w/ filters --- apps/search_indexes/filters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 63f1dbb8..6216b4dc 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -108,9 +108,9 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): tag_facets = [] preserve_ids = [] facet_name = '_filter_' + __field - all_tag_categories = TagCategoryDocument.search() \ + all_tag_categories = list(TagCategoryDocument.search() \ .filter('term', public=True) \ - .filter(Q('term', value_type=TagCategory.LIST) | Q('match', index_name='wine-color')) + .filter(Q('term', value_type=TagCategory.LIST) | Q('match', index_name='wine-color'))[0:100000]) for category in all_tag_categories: tags_to_remove = list(map(lambda t: str(t.id), category.tags)) qs = queryset.__copy__() From 2492000ae604fbdf3354516111f3e3d452e343c6 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 14 Jan 2020 21:30:21 +0300 Subject: [PATCH 023/119] try to fix issue w/ filters --- apps/search_indexes/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/filters.py b/apps/search_indexes/filters.py index 6216b4dc..988b8a54 100644 --- a/apps/search_indexes/filters.py +++ b/apps/search_indexes/filters.py @@ -110,7 +110,7 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend): facet_name = '_filter_' + __field all_tag_categories = list(TagCategoryDocument.search() \ .filter('term', public=True) \ - .filter(Q('term', value_type=TagCategory.LIST) | Q('match', index_name='wine-color'))[0:100000]) + .filter(Q('term', value_type=TagCategory.LIST) | Q('match', index_name='wine-color'))[0:1000]) for category in all_tag_categories: tags_to_remove = list(map(lambda t: str(t.id), category.tags)) qs = queryset.__copy__() From 42190e0e288dd9372151c274391970c01634a8a9 Mon Sep 17 00:00:00 2001 From: dormantman Date: Tue, 14 Jan 2020 21:57:57 +0300 Subject: [PATCH 024/119] Fix for front auth --- apps/authorization/serializers/common.py | 12 +++++++----- apps/utils/methods.py | 13 +++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index 4ebc1c99..ab1dc00d 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -43,11 +43,6 @@ class SignupSerializer(serializers.ModelSerializer): if account_models.User.objects.filter(username__iexact=value).exists(): raise serializers.ValidationError() - else: - value = utils_methods.username_random() - while account_models.User.objects.filter(username__iexact=value).exists(): - value = utils_methods.username_random() - return value def validate_email(self, value): @@ -67,6 +62,13 @@ class SignupSerializer(serializers.ModelSerializer): def create(self, validated_data): """Overridden create method""" + + username = validated_data.get('username') + if not username: + username = utils_methods.username_random() + while account_models.User.objects.filter(username__iexact=username).exists(): + username = utils_methods.username_random() + obj = account_models.User.objects.make( username=validated_data.get('username'), password=validated_data.get('password'), diff --git a/apps/utils/methods.py b/apps/utils/methods.py index 71101a9e..4928382f 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -3,9 +3,8 @@ import logging import random import re import string -from io import BytesIO -from PIL import Image from collections import namedtuple +from io import BytesIO import requests from django.conf import settings @@ -13,6 +12,7 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.gis.geos import Point from django.http.request import HttpRequest from django.utils.timezone import datetime +from PIL import Image from rest_framework import status from rest_framework.request import Request @@ -57,9 +57,14 @@ def username_validator(username: str) -> bool: def username_random(): """Generate random username""" - return '{letters}{digits}'.format( - letters=''.join([random.choice(string.ascii_lowercase) for _ in range(5)]), + username = list('{letters}{digits}'.format( + letters=''.join([random.choice(string.ascii_lowercase) for _ in range(4)]), digits=random.randrange(100, 1000) + )) + random.shuffle(username) + return '{first}{username}'.format( + first=random.choice(string.ascii_lowercase), + username=''.join(username) ) From 1c4e141383d15c6371e7f65e56c77c7ba84eb6c3 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 14 Jan 2020 22:04:11 +0300 Subject: [PATCH 025/119] fix import error for role serializer --- apps/account/serializers/back.py | 5 ++-- apps/account/serializers/common.py | 12 ++++---- apps/main/serializers/__init__.py | 1 + apps/main/serializers/back.py | 29 +++++++++++++++++++ .../{serializers.py => serializers/common.py} | 26 ----------------- apps/main/views/back.py | 5 ++-- 6 files changed, 42 insertions(+), 36 deletions(-) create mode 100644 apps/main/serializers/__init__.py create mode 100644 apps/main/serializers/back.py rename apps/main/{serializers.py => serializers/common.py} (93%) diff --git a/apps/account/serializers/back.py b/apps/account/serializers/back.py index 4b9787d1..18cf4ae9 100644 --- a/apps/account/serializers/back.py +++ b/apps/account/serializers/back.py @@ -1,9 +1,10 @@ """Back account serializers""" from rest_framework import serializers + from account import models from account.models import User +from account.serializers import RoleBaseSerializer from main.models import SiteSettings -# from account.serializers.common import RoleBaseSerializer class _SiteSettingsSerializer(serializers.ModelSerializer): @@ -18,7 +19,7 @@ class _SiteSettingsSerializer(serializers.ModelSerializer): class BackUserSerializer(serializers.ModelSerializer): last_country = _SiteSettingsSerializer(read_only=True) - # roles = RoleBaseSerializer(many=True, read_only=True) + roles = RoleBaseSerializer(many=True, read_only=True) class Meta: model = User diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index a3347b46..d9df65e8 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -8,13 +8,12 @@ from rest_framework import serializers from rest_framework import validators as rest_validators from account import models, tasks +from main.serializers.common import NavigationBarPermissionBaseSerializer from notification.models import Subscriber from utils import exceptions as utils_exceptions from utils import methods as utils_methods -from main.serializers import NavigationBarPermissionBaseSerializer -# User serializers class RoleBaseSerializer(serializers.ModelSerializer): """Serializer for model Role.""" role_display = serializers.CharField(source='get_role_display', read_only=True) @@ -100,12 +99,12 @@ class UserSerializer(serializers.ModelSerializer): tasks.change_email_address.delay( user_id=instance.id, country_code=self.context.get('request').country_code, - emails=[validated_data['email'],]) + emails=[validated_data['email'], ]) else: tasks.change_email_address( user_id=instance.id, country_code=self.context.get('request').country_code, - emails=[validated_data['email'],]) + emails=[validated_data['email'], ]) return instance @@ -232,6 +231,7 @@ class ChangeEmailSerializer(serializers.ModelSerializer): # Firebase Cloud Messaging serializers class FCMDeviceSerializer(serializers.ModelSerializer): """FCM Device model serializer""" + class Meta: model = FCMDevice fields = ('id', 'name', 'registration_id', 'device_id', @@ -251,8 +251,8 @@ class FCMDeviceSerializer(serializers.ModelSerializer): def __init__(self, *args, **kwargs): super(FCMDeviceSerializer, self).__init__(*args, **kwargs) self.fields['type'].help_text = ( - 'Should be one of ["%s"]' % - '", "'.join([i for i in self.fields['type'].choices])) + 'Should be one of ["%s"]' % + '", "'.join([i for i in self.fields['type'].choices])) def create(self, validated_data): user = self.context['request'].user diff --git a/apps/main/serializers/__init__.py b/apps/main/serializers/__init__.py new file mode 100644 index 00000000..ffc04a8d --- /dev/null +++ b/apps/main/serializers/__init__.py @@ -0,0 +1 @@ +from main.serializers.common import * diff --git a/apps/main/serializers/back.py b/apps/main/serializers/back.py new file mode 100644 index 00000000..58221fc0 --- /dev/null +++ b/apps/main/serializers/back.py @@ -0,0 +1,29 @@ +from rest_framework import serializers + +from account.models import User +from account.serializers import BackUserSerializer +from main import models + + +class PanelSerializer(serializers.ModelSerializer): + """Serializer for Custom panel.""" + user_id = serializers.PrimaryKeyRelatedField( + queryset=User.objects.all(), + source='user', + write_only=True + ) + user = BackUserSerializer(read_only=True) + + class Meta: + model = models.Panel + fields = [ + 'id', + 'name', + 'display', + 'description', + 'query', + 'created', + 'modified', + 'user', + 'user_id' + ] diff --git a/apps/main/serializers.py b/apps/main/serializers/common.py similarity index 93% rename from apps/main/serializers.py rename to apps/main/serializers/common.py index afb77aa8..a954c649 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers/common.py @@ -2,8 +2,6 @@ from django.contrib.contenttypes.models import ContentType from rest_framework import serializers -from account.models import User -from account.serializers.back import BackUserSerializer from location.serializers import CountrySerializer from main import models from tag.serializers import TagBackOfficeSerializer @@ -314,30 +312,6 @@ class ContentTypeBackSerializer(serializers.ModelSerializer): fields = '__all__' -class PanelSerializer(serializers.ModelSerializer): - """Serializer for Custom panel.""" - user_id = serializers.PrimaryKeyRelatedField( - queryset=User.objects.all(), - source='user', - write_only=True - ) - user = BackUserSerializer(read_only=True) - - class Meta: - model = models.Panel - fields = [ - 'id', - 'name', - 'display', - 'description', - 'query', - 'created', - 'modified', - 'user', - 'user_id' - ] - - class PanelExecuteSerializer(serializers.ModelSerializer): """Panel execute serializer.""" diff --git a/apps/main/views/back.py b/apps/main/views/back.py index b17a692c..a8974721 100644 --- a/apps/main/views/back.py +++ b/apps/main/views/back.py @@ -6,6 +6,7 @@ from rest_framework.generics import get_object_or_404 from rest_framework.response import Response from main import serializers +from main.serializers.back import PanelSerializer from main import tasks from main.filters import AwardFilter from main.models import Award, Footer, PageType, Panel, SiteFeature, Feature @@ -108,7 +109,7 @@ class PanelsListCreateView(generics.ListCreateAPIView): permission_classes = ( permissions.IsAdminUser, ) - serializer_class = serializers.PanelSerializer + serializer_class = PanelSerializer queryset = Panel.objects.all() @@ -117,7 +118,7 @@ class PanelsRUDView(generics.RetrieveUpdateDestroyAPIView): permission_classes = ( permissions.IsAdminUser, ) - serializer_class = serializers.PanelSerializer + serializer_class = PanelSerializer queryset = Panel.objects.all() From 0cf62353becc36e2fc5387413bc89003a2a89dff Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 14 Jan 2020 22:06:53 +0300 Subject: [PATCH 026/119] fix role field --- apps/account/serializers/back.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/account/serializers/back.py b/apps/account/serializers/back.py index 18cf4ae9..a5a0ccdb 100644 --- a/apps/account/serializers/back.py +++ b/apps/account/serializers/back.py @@ -44,7 +44,7 @@ class BackUserSerializer(serializers.ModelSerializer): 'locale', 'last_ip', 'last_country', - # 'roles', + 'roles', ) extra_kwargs = { 'password': {'write_only': True}, From 67ab06022f0fcd61c7aaa370155f1cafbb9e448e Mon Sep 17 00:00:00 2001 From: dormantman Date: Wed, 15 Jan 2020 01:01:27 +0300 Subject: [PATCH 027/119] Added short translated weekday for timetable --- apps/timetable/models.py | 12 ++++++++---- apps/timetable/serialziers.py | 37 +++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/apps/timetable/models.py b/apps/timetable/models.py index c9295b3b..62f3a3e9 100644 --- a/apps/timetable/models.py +++ b/apps/timetable/models.py @@ -1,6 +1,7 @@ +from datetime import datetime, time + from django.db import models from django.utils.translation import gettext_lazy as _ -from datetime import time, datetime from utils.models import ProjectBaseMixin @@ -24,7 +25,8 @@ class Timetable(ProjectBaseMixin): (THURSDAY, _('Thursday')), (FRIDAY, _('Friday')), (SATURDAY, _('Saturday')), - (SUNDAY, _('Sunday'))) + (SUNDAY, _('Sunday')) + ) weekday = models.PositiveSmallIntegerField(choices=WEEKDAYS_CHOICES, verbose_name=_('Week day')) @@ -61,11 +63,13 @@ class Timetable(ProjectBaseMixin): @property def closed_at_indexing(self): - return datetime.combine(time=self.closed_at, date=datetime(1970, 1, 1 + self.weekday).date()) if self.closed_at else None + return datetime.combine(time=self.closed_at, + date=datetime(1970, 1, 1 + self.weekday).date()) if self.closed_at else None @property def opening_at_indexing(self): - return datetime.combine(time=self.opening_at, date=datetime(1970, 1, 1 + self.weekday).date()) if self.opening_at else None + return datetime.combine(time=self.opening_at, + date=datetime(1970, 1, 1 + self.weekday).date()) if self.opening_at else None @property def opening_time(self): diff --git a/apps/timetable/serialziers.py b/apps/timetable/serialziers.py index 305be0ec..616abddf 100644 --- a/apps/timetable/serialziers.py +++ b/apps/timetable/serialziers.py @@ -1,4 +1,7 @@ """Serializer for app timetable""" + +import datetime + from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -11,8 +14,8 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): NULLABLE_FIELDS = ['lunch_start', 'lunch_end', 'dinner_start', 'dinner_end', 'opening_at', 'closed_at'] - weekday_display = serializers.CharField(source='get_weekday_display', - read_only=True) + weekday_display = serializers.CharField(source='get_weekday_display', read_only=True) + weekday_display_short = serializers.SerializerMethodField(read_only=True) lunch_start = serializers.TimeField(required=False) lunch_end = serializers.TimeField(required=False) @@ -29,6 +32,7 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): fields = [ 'id', 'weekday_display', + 'weekday_display_short', 'weekday', 'lunch_start', 'lunch_end', @@ -39,15 +43,21 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): 'establishment_id' ] + def get_weekday_display_short(self, obj: Timetable): + """Translated short day of the week""" + monday = datetime.date(2020, 1, 6) + with_weekday = monday + datetime.timedelta(days=obj.weekday) + return _(with_weekday.strftime("%a")) + def validate(self, attrs): """Override validate method""" - establishment_pk = self.context.get('request')\ - .parser_context.get('view')\ - .kwargs.get('pk') + establishment_pk = self.context.get('request') \ + .parser_context.get('view') \ + .kwargs.get('pk') - establishment_slug = self.context.get('request')\ - .parser_context.get('view')\ - .kwargs.get('slug') + establishment_slug = self.context.get('request') \ + .parser_context.get('view') \ + .kwargs.get('slug') search_kwargs = {'pk': establishment_pk} if establishment_pk else {'slug': establishment_slug} @@ -91,13 +101,20 @@ class ScheduleCreateSerializer(ScheduleRUDSerializer): class TimetableSerializer(serializers.ModelSerializer): """Serailzier for Timetable model.""" - weekday_display = serializers.CharField(source='get_weekday_display', - read_only=True) + weekday_display = serializers.CharField(source='get_weekday_display', read_only=True) + weekday_display_short = serializers.SerializerMethodField(read_only=True) class Meta: model = Timetable fields = ( 'id', 'weekday_display', + 'weekday_display_short', 'works_at_noon', ) + + def get_weekday_display_short(self, obj: Timetable): + """Translated short day of the week""" + monday = datetime.date(2020, 1, 6) + with_weekday = monday + datetime.timedelta(days=obj.weekday) + return _(with_weekday.strftime("%a")) From 660a46b8957b7ca31a696855be1861df0b3cb4fb Mon Sep 17 00:00:00 2001 From: dormantman Date: Wed, 15 Jan 2020 01:10:52 +0300 Subject: [PATCH 028/119] Added short weekday to model --- apps/search_indexes/documents/establishment.py | 1 + apps/timetable/models.py | 9 ++++++++- apps/timetable/serialziers.py | 16 ++-------------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 20185b9c..15568862 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -153,6 +153,7 @@ class EstablishmentDocument(Document): 'id': fields.IntegerField(attr='id'), 'weekday': fields.IntegerField(attr='weekday'), 'weekday_display': fields.KeywordField(attr='get_weekday_display'), + 'weekday_display_short': fields.KeywordField(attr='weekday_display_short'), 'closed_at': fields.KeywordField(attr='closed_at_str'), 'opening_at': fields.KeywordField(attr='opening_at_str'), 'closed_at_indexing': fields.DateField(), diff --git a/apps/timetable/models.py b/apps/timetable/models.py index 62f3a3e9..ba4fadad 100644 --- a/apps/timetable/models.py +++ b/apps/timetable/models.py @@ -1,4 +1,4 @@ -from datetime import datetime, time +from datetime import datetime, time, date, timedelta from django.db import models from django.utils.translation import gettext_lazy as _ @@ -53,6 +53,13 @@ class Timetable(ProjectBaseMixin): f'works_at_noon - {self.works_at_noon}, ' \ f'works_at_afternoon: {self.works_at_afternoon})' + @property + def weekday_display_short(self): + """Translated short day of the week""" + monday = date(2020, 1, 6) + with_weekday = monday + timedelta(days=self.weekday) + return _(with_weekday.strftime("%a")) + @property def closed_at_str(self): return str(self.closed_at) if self.closed_at else None diff --git a/apps/timetable/serialziers.py b/apps/timetable/serialziers.py index 616abddf..a3183543 100644 --- a/apps/timetable/serialziers.py +++ b/apps/timetable/serialziers.py @@ -15,7 +15,7 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): 'dinner_end', 'opening_at', 'closed_at'] weekday_display = serializers.CharField(source='get_weekday_display', read_only=True) - weekday_display_short = serializers.SerializerMethodField(read_only=True) + weekday_display_short = serializers.CharField(read_only=True) lunch_start = serializers.TimeField(required=False) lunch_end = serializers.TimeField(required=False) @@ -43,12 +43,6 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): 'establishment_id' ] - def get_weekday_display_short(self, obj: Timetable): - """Translated short day of the week""" - monday = datetime.date(2020, 1, 6) - with_weekday = monday + datetime.timedelta(days=obj.weekday) - return _(with_weekday.strftime("%a")) - def validate(self, attrs): """Override validate method""" establishment_pk = self.context.get('request') \ @@ -102,7 +96,7 @@ class ScheduleCreateSerializer(ScheduleRUDSerializer): class TimetableSerializer(serializers.ModelSerializer): """Serailzier for Timetable model.""" weekday_display = serializers.CharField(source='get_weekday_display', read_only=True) - weekday_display_short = serializers.SerializerMethodField(read_only=True) + weekday_display_short = serializers.CharField(read_only=True) class Meta: model = Timetable @@ -112,9 +106,3 @@ class TimetableSerializer(serializers.ModelSerializer): 'weekday_display_short', 'works_at_noon', ) - - def get_weekday_display_short(self, obj: Timetable): - """Translated short day of the week""" - monday = datetime.date(2020, 1, 6) - with_weekday = monday + datetime.timedelta(days=obj.weekday) - return _(with_weekday.strftime("%a")) From 19b9e636c13f4e3adf3009064211bbad8bb855a0 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 15 Jan 2020 09:20:09 +0300 Subject: [PATCH 029/119] phones for cascade establishment --- apps/establishment/serializers/back.py | 34 ++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 54f46780..893e9439 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -16,7 +16,7 @@ from utils.serializers import TimeZoneChoiceField def phones_handler(phones_list, establishment): """ - create or update phones for establishment 35016 string + create or update phones for establishment """ ContactPhone.objects.filter(establishment=establishment).delete() @@ -37,8 +37,6 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria queryset=models.Address.objects.all(), write_only=True ) - phones = model_serializers.ContactPhonesSerializer(read_only=True, - many=True, ) emails = model_serializers.ContactEmailsSerializer(read_only=True, many=True, ) socials = model_serializers.SocialNetworkRelatedSerializers(read_only=True, @@ -48,10 +46,12 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria address_id = serializers.PrimaryKeyRelatedField(write_only=True, source='address', queryset=Address.objects.all()) tz = TimeZoneChoiceField() - phones_list = serializers.ListField( - child=serializers.CharField(max_length=20), + phones = serializers.ListField( + source='contact_phones', + allow_null=True, allow_empty=True, - write_only=True, + child=serializers.CharField(max_length=128), + required=False, ) class Meta: @@ -78,11 +78,13 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria 'tags', 'tz', 'address_id', - 'phones_list', ] def create(self, validated_data): - phones_list = validated_data.pop('phones_list') + phones_list = [] + if 'contact_phones' in validated_data: + phones_list = validated_data.pop('contact_phones') + instance = super().create(validated_data) phones_handler(phones_list, instance) return instance @@ -96,17 +98,17 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): queryset=models.EstablishmentType.objects.all(), write_only=True ) address = AddressDetailSerializer() - phones = model_serializers.ContactPhonesSerializer(read_only=False, - many=True, ) emails = model_serializers.ContactEmailsSerializer(read_only=False, many=True, ) socials = model_serializers.SocialNetworkRelatedSerializers(read_only=False, many=True, ) type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type') - phones_list = serializers.ListField( - child=serializers.CharField(max_length=20), + phones = serializers.ListField( + source='contact_phones', + allow_null=True, allow_empty=True, - write_only=True, + child=serializers.CharField(max_length=128), + required=False, ) class Meta: @@ -127,11 +129,13 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): 'is_publish', 'address', 'tags', - 'phones_list', ] def update(self, instance, validated_data): - phones_list = validated_data.pop('phones_list') + phones_list = [] + if 'contact_phones' in validated_data: + phones_list = validated_data.pop('contact_phones') + instance = super().update(instance, validated_data) phones_handler(phones_list, instance) return instance From 210070b7cfe85c56821d15213c273b58f5493a2e Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Wed, 15 Jan 2020 06:45:42 +0000 Subject: [PATCH 030/119] fix regex for find images --- apps/utils/methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/utils/methods.py b/apps/utils/methods.py index 4928382f..919ce64b 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -189,7 +189,7 @@ def section_name_into_index_name(section_name: str): def get_url_images_in_text(text): """Find images urls in text""" - return re.findall(r'(?:http:|https:)?//.*\.(?:png|jpg|svg)', text) + return re.findall(r'[^"\'=\s]+\.(jpe?g|png|gif|svg)', text) def get_image_meta_by_url(url) -> (int, int, int): From 7bc998ca94c42a7e416be2d7c3980ede1df96571 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Wed, 15 Jan 2020 06:57:59 +0000 Subject: [PATCH 031/119] fix regex for find images --- apps/utils/methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/utils/methods.py b/apps/utils/methods.py index 919ce64b..741fd23b 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -189,7 +189,7 @@ def section_name_into_index_name(section_name: str): def get_url_images_in_text(text): """Find images urls in text""" - return re.findall(r'[^"\'=\s]+\.(jpe?g|png|gif|svg)', text) + return re.findall(r'[^\"\'=\s]+\.jpe?g|png|gif|svg', text) def get_image_meta_by_url(url) -> (int, int, int): From e4123d3ce9afa6073f3b790c3257fbb5a86cc773 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 15 Jan 2020 10:47:18 +0300 Subject: [PATCH 032/119] preview_image to establishment and product type and subtype --- apps/establishment/serializers/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index d53fca1e..433ff160 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -256,7 +256,6 @@ class _EstablishmentAddressShortSerializer(serializers.ModelSerializer): city = CitySerializer(source='address.city', allow_null=True) establishment_type = EstablishmentTypeGeoSerializer() establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True) - currency = CurrencySerializer(read_only=True) address = AddressBaseSerializer(read_only=True) class Meta: From 32ad122d9a5690c4a5f319b7321e06813e8388a9 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 15 Jan 2020 13:15:27 +0300 Subject: [PATCH 033/119] set priority in Feature model as blank, and remove uniqueness constrait --- .../main/migrations/0047_auto_20200115_1013.py | 18 ++++++++++++++++++ apps/main/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 apps/main/migrations/0047_auto_20200115_1013.py diff --git a/apps/main/migrations/0047_auto_20200115_1013.py b/apps/main/migrations/0047_auto_20200115_1013.py new file mode 100644 index 00000000..83d025b5 --- /dev/null +++ b/apps/main/migrations/0047_auto_20200115_1013.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2020-01-15 10:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0046_auto_20200114_1218'), + ] + + operations = [ + migrations.AlterField( + model_name='feature', + name='priority', + field=models.IntegerField(blank=True, default=None, null=True), + ), + ] diff --git a/apps/main/models.py b/apps/main/models.py index 6882568f..bae7d61a 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -114,7 +114,7 @@ class Feature(ProjectBaseMixin, PlatformMixin): """Feature model.""" slug = models.SlugField(max_length=255, unique=True) - priority = models.IntegerField(unique=True, null=True, default=None) + priority = models.IntegerField(blank=True, null=True, default=None) route = models.ForeignKey('PageType', on_delete=models.PROTECT, null=True, default=None) site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature') old_id = models.IntegerField(null=True, blank=True) From 8cb0f51802f1b4d2941a8b062b750a0eb8c695ad Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 15 Jan 2020 13:22:42 +0300 Subject: [PATCH 034/119] fix transfer command transfer_city_gallery, add field image to City model, fix serializer CityDetailSerializer --- apps/establishment/serializers/common.py | 6 ++-- apps/location/migrations/0034_city_image.py | 20 +++++++++++++ apps/location/models.py | 31 ++++++++++++++++++++- apps/location/serializers/common.py | 17 +++++++++-- apps/location/transfer_data.py | 19 ++++++------- apps/location/views/back.py | 6 ++-- apps/location/views/common.py | 10 +++---- project/settings/local.py | 28 +++++++++---------- requirements/base.txt | 1 - requirements/development.txt | 6 +++- 10 files changed, 102 insertions(+), 42 deletions(-) create mode 100644 apps/location/migrations/0034_city_image.py diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 433ff160..eef4dbe7 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -6,7 +6,7 @@ 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, CitySerializer, AddressDetailSerializer, \ +from location.serializers import AddressBaseSerializer, CityBaseSerializer, AddressDetailSerializer, \ CityShortSerializer from location.serializers import EstablishmentWineRegionBaseSerializer, \ EstablishmentWineOriginBaseSerializer @@ -231,7 +231,7 @@ class EstablishmentEmployeeCreateSerializer(serializers.ModelSerializer): class EstablishmentShortSerializer(serializers.ModelSerializer): """Short serializer for establishment.""" - city = CitySerializer(source='address.city', allow_null=True) + city = CityBaseSerializer(source='address.city', allow_null=True) establishment_type = EstablishmentTypeGeoSerializer() establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True) currency = CurrencySerializer(read_only=True) @@ -253,7 +253,7 @@ class EstablishmentShortSerializer(serializers.ModelSerializer): class _EstablishmentAddressShortSerializer(serializers.ModelSerializer): """Short serializer for establishment.""" - city = CitySerializer(source='address.city', allow_null=True) + city = CityBaseSerializer(source='address.city', allow_null=True) establishment_type = EstablishmentTypeGeoSerializer() establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True) address = AddressBaseSerializer(read_only=True) diff --git a/apps/location/migrations/0034_city_image.py b/apps/location/migrations/0034_city_image.py new file mode 100644 index 00000000..9c27287c --- /dev/null +++ b/apps/location/migrations/0034_city_image.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.7 on 2020-01-15 09:45 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('gallery', '0008_merge_20191212_0752'), + ('location', '0033_merge_20191224_0920'), + ] + + operations = [ + migrations.AddField( + model_name='city', + name='image', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='city_image', to='gallery.Image', verbose_name='image instance of model Image'), + ), + ] diff --git a/apps/location/models.py b/apps/location/models.py index cdc9ada5..162e72d2 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -75,7 +75,6 @@ class Country(TranslatedFieldsMixin, return self.id - class RegionQuerySet(models.QuerySet): """QuerySet for model Region.""" @@ -168,7 +167,13 @@ class City(GalleryMixin, models.Model): map_ref = models.CharField(max_length=255, blank=True, null=True) situation = models.CharField(max_length=255, blank=True, null=True) + # deprecated + # todo: remove gallery after move to image gallery = models.ManyToManyField('gallery.Image', through='location.CityGallery', blank=True) + image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL, + blank=True, null=True, default=None, + related_name='city_image', + verbose_name=_('image instance of model Image')) mysql_id = models.IntegerField(blank=True, null=True, default=None) @@ -181,6 +186,30 @@ class City(GalleryMixin, models.Model): def __str__(self): return self.name + @property + def image_object(self): + """Return image object.""" + return self.image.image if self.image else None + + @property + def crop_image(self): + if hasattr(self, 'image') and hasattr(self, '_meta'): + if self.image: + image_property = { + 'id': self.image.id, + 'title': self.image.title, + 'original_url': self.image.image.url, + 'orientation_display': self.image.get_orientation_display(), + 'auto_crop_images': {}, + } + crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES + if p.startswith(self._meta.model_name.lower())] + for crop in crop_parameters: + image_property['auto_crop_images'].update( + {crop: self.image.get_image_url(crop)} + ) + return image_property + class CityGallery(IntermediateGalleryModelMixin): """Gallery for model City.""" diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index 65c30b46..9f8613c4 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -3,7 +3,7 @@ from django.contrib.gis.geos import Point from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from location import models -from utils.serializers import TranslatedField +from utils.serializers import TranslatedField, ImageBaseSerializer class CountrySerializer(serializers.ModelSerializer): @@ -70,7 +70,7 @@ class CityShortSerializer(serializers.ModelSerializer): ) -class CitySerializer(serializers.ModelSerializer): +class CityBaseSerializer(serializers.ModelSerializer): """City serializer.""" region = RegionSerializer(read_only=True) region_id = serializers.PrimaryKeyRelatedField( @@ -99,6 +99,17 @@ class CitySerializer(serializers.ModelSerializer): ] +class CityDetailSerializer(CityBaseSerializer): + """Serializer for detail view.""" + image = ImageBaseSerializer(source='crop_image', read_only=True) + + class Meta(CityBaseSerializer.Meta): + """Meta class.""" + fields = CityBaseSerializer.Meta.fields + [ + 'image', + ] + + class AddressBaseSerializer(serializers.ModelSerializer): """Serializer for address obj in related objects.""" @@ -154,7 +165,7 @@ class AddressDetailSerializer(AddressBaseSerializer): city_id = serializers.PrimaryKeyRelatedField( source='city', write_only=True, queryset=models.City.objects.all()) - city = CitySerializer(read_only=True) + city = CityBaseSerializer(read_only=True) class Meta(AddressBaseSerializer.Meta): """Meta class.""" diff --git a/apps/location/transfer_data.py b/apps/location/transfer_data.py index 54bf2301..ba6c535f 100644 --- a/apps/location/transfer_data.py +++ b/apps/location/transfer_data.py @@ -557,7 +557,7 @@ def remove_old_records(): def transfer_city_gallery(): created_counter = 0 cities_not_exists = {} - gallery_obj_exists_counter = 0 + cities_has_same_image = 0 city_gallery = transfer_models.CityPhotos.objects.exclude(city__isnull=True) \ .exclude(city__country_code_2__isnull=True) \ @@ -565,7 +565,7 @@ def transfer_city_gallery(): .exclude(city__region_code__isnull=True) \ .exclude(city__region_code__iexact='') \ .values_list('city_id', 'attachment_suffix_url') - for old_city_id, image_suffix_url in city_gallery: + for old_city_id, image_suffix_url in tqdm(city_gallery): city = City.objects.filter(old_id=old_city_id) if city.exists(): city = city.first() @@ -575,19 +575,18 @@ def transfer_city_gallery(): 'orientation': Image.HORIZONTAL, 'title': f'{city.name} - {image_suffix_url}', }) - city_gallery, created = CityGallery.objects.get_or_create(image=image, - city=city, - is_main=True) - if created: + if city.image != image: + city.image = image + city.save() created_counter += 1 else: - gallery_obj_exists_counter += 1 + cities_has_same_image += 1 else: cities_not_exists.update({'city_old_id': old_city_id}) print(f'Created: {created_counter}\n' f'City not exists: {cities_not_exists}\n' - f'Already added: {gallery_obj_exists_counter}') + f'City has same image: {cities_has_same_image}') @atomic @@ -796,7 +795,6 @@ def set_unused_regions(): ) - data_types = { "dictionaries": [ # transfer_countries, @@ -824,12 +822,11 @@ data_types = { remove_old_records ], "fill_city_gallery": [ - transfer_city_gallery + transfer_city_gallery, ], "add_fake_country": [ add_fake_country, ], - "setup_clean_db": [setup_clean_db], "set_unused_regions": [set_unused_regions], "update_fake_country_flag": [update_fake_country_flag] diff --git a/apps/location/views/back.py b/apps/location/views/back.py index 125c2b0b..b02c79aa 100644 --- a/apps/location/views/back.py +++ b/apps/location/views/back.py @@ -36,7 +36,7 @@ class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIV # City class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView): """Create view for model City.""" - serializer_class = serializers.CitySerializer + serializer_class = serializers.CityBaseSerializer permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin] queryset = models.City.objects.all() filter_class = filters.CityBackFilter @@ -52,7 +52,7 @@ class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView): class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView): """Create view for model City.""" - serializer_class = serializers.CitySerializer + serializer_class = serializers.CityBaseSerializer permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin] queryset = models.City.objects.all()\ .annotate(locale_name=KeyTextTransform(get_current_locale(), 'name_translated'))\ @@ -63,7 +63,7 @@ class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView): class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView): """RUD view for model City.""" - serializer_class = serializers.CitySerializer + serializer_class = serializers.CityDetailSerializer permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin] diff --git a/apps/location/views/common.py b/apps/location/views/common.py index 660a1dbe..d9d5520b 100644 --- a/apps/location/views/common.py +++ b/apps/location/views/common.py @@ -85,18 +85,18 @@ class RegionUpdateView(RegionViewMixin, generics.UpdateAPIView): # City class CityCreateView(CityViewMixin, generics.CreateAPIView): """Create view for model City""" - serializer_class = serializers.CitySerializer + serializer_class = serializers.CityBaseSerializer class CityRetrieveView(CityViewMixin, generics.RetrieveAPIView): """Retrieve view for model City""" - serializer_class = serializers.CitySerializer + serializer_class = serializers.CityDetailSerializer class CityListView(CityViewMixin, generics.ListAPIView): """List view for model City""" permission_classes = (permissions.AllowAny,) - serializer_class = serializers.CitySerializer + serializer_class = serializers.CityBaseSerializer def get_queryset(self): qs = super().get_queryset() @@ -107,12 +107,12 @@ class CityListView(CityViewMixin, generics.ListAPIView): class CityDestroyView(CityViewMixin, generics.DestroyAPIView): """Destroy view for model City""" - serializer_class = serializers.CitySerializer + serializer_class = serializers.CityBaseSerializer class CityUpdateView(CityViewMixin, generics.UpdateAPIView): """Update view for model City""" - serializer_class = serializers.CitySerializer + serializer_class = serializers.CityBaseSerializer # Address diff --git a/project/settings/local.py b/project/settings/local.py index a5c0ec8a..2ad132f1 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -43,15 +43,15 @@ DATABASES = { 'options': '-c search_path=gm,public' }, }, - 'legacy': { - 'ENGINE': 'django.db.backends.mysql', - # 'HOST': '172.22.0.1', - 'HOST': 'mysql_db', - 'PORT': 3306, - 'NAME': 'dev', - 'USER': 'dev', - 'PASSWORD': 'octosecret123' - }, + 'legacy': { + 'ENGINE': 'django.db.backends.mysql', + # 'HOST': '172.22.0.1', + 'HOST': 'mysql_db', + 'PORT': 3306, + 'NAME': 'dev', + 'USER': 'dev', + 'PASSWORD': 'octosecret123' + }, } @@ -84,11 +84,11 @@ LOGGING = { 'py.warnings': { 'handlers': ['console'], }, - 'django.db.backends': { - 'handlers': ['console', ], - 'level': 'DEBUG', - 'propagate': False, - }, + # 'django.db.backends': { + # 'handlers': ['console', ], + # 'level': 'DEBUG', + # 'propagate': False, + # }, } } diff --git a/requirements/base.txt b/requirements/base.txt index f5bbf2d5..25a0256c 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -49,7 +49,6 @@ django-storages==1.7.2 sorl-thumbnail==12.5.0 - PyYAML==5.1.2 # temp solution diff --git a/requirements/development.txt b/requirements/development.txt index 77c3d73c..4763063b 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -1,4 +1,8 @@ -r base.txt ipdb ipython -mysqlclient==1.4.4 \ No newline at end of file +mysqlclient==1.4.4 + +pyparsing +graphviz +pydot \ No newline at end of file From 7533579172839662fd65f9c342e625d1fc4d258e Mon Sep 17 00:00:00 2001 From: dormantman Date: Wed, 15 Jan 2020 13:24:53 +0300 Subject: [PATCH 035/119] Added short weekday to tag filters --- apps/tag/views.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/tag/views.py b/apps/tag/views.py index 4f1cd9ba..7467cbb6 100644 --- a/apps/tag/views.py +++ b/apps/tag/views.py @@ -90,6 +90,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): params_type = query_params.get('product_type') week_days = tuple(map(_, ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"))) + short_week_days = tuple(map(_, ("Mon", "Tur", "Wed", "Thu", "Fri", "Sat", "Sun"))) flags = ('toque_number', 'wine_region', 'works_noon', 'works_evening', 'works_now', 'works_at_weekday') filter_flags = {flag_name: False for flag_name in flags} additional_flags = [] @@ -155,7 +156,8 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): "filters": [{ "id": weekday, "index_name": week_days[weekday].lower(), - "label_translated": week_days[weekday] + "label_translated": week_days[weekday], + "label_translated_short": short_week_days[weekday], } for weekday in range(7)] } result_list.append(works_noon) @@ -170,7 +172,8 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): "filters": [{ "id": weekday, "index_name": week_days[weekday].lower(), - "label_translated": week_days[weekday] + "label_translated": week_days[weekday], + "label_translated_short": short_week_days[weekday], } for weekday in range(7)] } result_list.append(works_evening) @@ -193,7 +196,8 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): "filters": [{ "id": weekday, "index_name": week_days[weekday].lower(), - "label_translated": week_days[weekday] + "label_translated": week_days[weekday], + "label_translated_short": short_week_days[weekday], } for weekday in range(7)] } result_list.append(works_at_weekday) From 42885e8b4c16a9b5b32dae8b8faba094880c6619 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 15 Jan 2020 13:45:15 +0300 Subject: [PATCH 036/119] International carousel --- apps/main/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/main/models.py b/apps/main/models.py index bae7d61a..a9e90895 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -230,6 +230,8 @@ class CarouselQuerySet(models.QuerySet): def by_country_code(self, code): """Filter collection by country code.""" + if code in settings.INTERNATIONAL_COUNTRY_CODES: + return self.filter(is_international=True) return self.filter(country__code=code) def get_international(self): From 8f206f2a9cfc907e440546a1875ce81b1db55756 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 15 Jan 2020 14:20:33 +0300 Subject: [PATCH 037/119] remove city gallery --- .../migrations/0035_auto_20200115_1117.py | 20 ++++++++ apps/location/models.py | 24 +-------- apps/location/serializers/back.py | 46 ----------------- apps/location/serializers/common.py | 4 ++ apps/location/urls/back.py | 5 -- apps/location/views/back.py | 51 ------------------- 6 files changed, 25 insertions(+), 125 deletions(-) create mode 100644 apps/location/migrations/0035_auto_20200115_1117.py diff --git a/apps/location/migrations/0035_auto_20200115_1117.py b/apps/location/migrations/0035_auto_20200115_1117.py new file mode 100644 index 00000000..1f88538d --- /dev/null +++ b/apps/location/migrations/0035_auto_20200115_1117.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.7 on 2020-01-15 11:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0034_city_image'), + ] + + operations = [ + migrations.RemoveField( + model_name='city', + name='gallery', + ), + migrations.DeleteModel( + name='CityGallery', + ), + ] diff --git a/apps/location/models.py b/apps/location/models.py index 162e72d2..0fa3cdd1 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -143,7 +143,7 @@ class CityQuerySet(models.QuerySet): return self.filter(country__code=code) -class City(GalleryMixin, models.Model): +class City(models.Model): """Region model.""" name = models.CharField(_('name'), max_length=250) name_translated = TJSONField(blank=True, null=True, default=None, @@ -166,10 +166,6 @@ class City(GalleryMixin, models.Model): map2 = models.CharField(max_length=255, blank=True, null=True) map_ref = models.CharField(max_length=255, blank=True, null=True) situation = models.CharField(max_length=255, blank=True, null=True) - - # deprecated - # todo: remove gallery after move to image - gallery = models.ManyToManyField('gallery.Image', through='location.CityGallery', blank=True) image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL, blank=True, null=True, default=None, related_name='city_image', @@ -211,24 +207,6 @@ class City(GalleryMixin, models.Model): return image_property -class CityGallery(IntermediateGalleryModelMixin): - """Gallery for model City.""" - city = models.ForeignKey(City, null=True, - related_name='city_gallery', - on_delete=models.CASCADE, - verbose_name=_('city')) - image = models.ForeignKey('gallery.Image', null=True, - related_name='city_gallery', - on_delete=models.CASCADE, - verbose_name=_('image')) - - class Meta: - """CityGallery meta class.""" - verbose_name = _('city gallery') - verbose_name_plural = _('city galleries') - unique_together = (('city', 'is_main'), ('city', 'image')) - - class Address(models.Model): """Address model.""" city = models.ForeignKey(City, verbose_name=_('city'), on_delete=models.CASCADE) diff --git a/apps/location/serializers/back.py b/apps/location/serializers/back.py index 8f231d69..c178f7fd 100644 --- a/apps/location/serializers/back.py +++ b/apps/location/serializers/back.py @@ -1,8 +1,5 @@ from location import models from location.serializers import common -from rest_framework import serializers -from gallery.models import Image -from django.utils.translation import gettext_lazy as _ class AddressCreateSerializer(common.AddressDetailSerializer): @@ -21,46 +18,3 @@ class CountryBackSerializer(common.CountrySerializer): 'name', 'country_id' ] - - -class CityGallerySerializer(serializers.ModelSerializer): - """Serializer class for model CityGallery.""" - - class Meta: - """Meta class""" - - model = models.CityGallery - fields = [ - 'id', - 'is_main', - ] - - @property - def request_kwargs(self): - """Get url kwargs from request.""" - return self.context.get('request').parser_context.get('kwargs') - - def validate(self, attrs): - """Override validate method.""" - city_pk = self.request_kwargs.get('pk') - image_id = self.request_kwargs.get('image_id') - - city_qs = models.City.objects.filter(pk=city_pk) - image_qs = Image.objects.filter(id=image_id) - - if not city_qs.exists(): - raise serializers.ValidationError({'detail': _('City not found')}) - - if not image_qs.exists(): - raise serializers.ValidationError({'detail': _('Image not found')}) - - city = city_qs.first() - image = image_qs.first() - - if image in city.gallery.all(): - raise serializers.ValidationError({'detail': _('Image is already added.')}) - - attrs['city'] = city - attrs['image'] = image - - return attrs diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index 9f8613c4..e466e62f 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -96,7 +96,11 @@ class CityBaseSerializer(serializers.ModelSerializer): 'country', 'postal_code', 'is_island', + 'image', ] + extra_fields = { + 'image': {'write_only': True} + } class CityDetailSerializer(CityBaseSerializer): diff --git a/apps/location/urls/back.py b/apps/location/urls/back.py index 2434dd26..b1c0d5ca 100644 --- a/apps/location/urls/back.py +++ b/apps/location/urls/back.py @@ -12,11 +12,6 @@ urlpatterns = [ path('cities/', views.CityListCreateView.as_view(), name='city-list-create'), path('cities/all/', views.CityListSearchView.as_view(), name='city-list-create'), path('cities//', views.CityRUDView.as_view(), name='city-retrieve'), - path('cities//gallery/', views.CityGalleryListView.as_view(), - name='gallery-list'), - path('cities//gallery//', - views.CityGalleryCreateDestroyView.as_view(), - name='gallery-create-destroy'), path('countries/', views.CountryListCreateView.as_view(), name='country-list-create'), path('countries//', views.CountryRUDView.as_view(), name='country-retrieve'), diff --git a/apps/location/views/back.py b/apps/location/views/back.py index b02c79aa..a0d1bd36 100644 --- a/apps/location/views/back.py +++ b/apps/location/views/back.py @@ -67,57 +67,6 @@ class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView): permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin] -class CityGalleryCreateDestroyView(common.CityViewMixin, - CreateDestroyGalleryViewMixin): - """Resource for a create gallery for product for back-office users.""" - serializer_class = serializers.CityGallerySerializer - permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin] - - def get_object(self): - """ - Returns the object the view is displaying. - """ - city_qs = self.filter_queryset(self.get_queryset()) - - city = get_object_or_404(city_qs, pk=self.kwargs.get('pk')) - gallery = get_object_or_404(city.city_gallery, image_id=self.kwargs.get('image_id')) - - # May raise a permission denied - self.check_object_permissions(self.request, gallery) - - return gallery - - def create(self, request, *args, **kwargs): - try: - return super(CityGalleryCreateDestroyView, self).create(request, *args, **kwargs) - except IntegrityError as e: - if not 'unique constraint' in e.args[0]: - raise e - models.CityGallery.objects.filter(city=kwargs['pk'], is_main=request.data['is_main']).delete() - return super(CityGalleryCreateDestroyView, self).create(request, *args, **kwargs) - - -class CityGalleryListView(common.CityViewMixin, - generics.ListAPIView): - """Resource for returning gallery for product for back-office users.""" - serializer_class = ImageBaseSerializer - permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin] - - def get_object(self): - """Override get_object method.""" - qs = super(CityGalleryListView, self).get_queryset() - city = get_object_or_404(qs, pk=self.kwargs['pk']) - - # May raise a permission denied - self.check_object_permissions(self.request, city) - - return city - - def get_queryset(self): - """Override get_queryset method.""" - return self.get_object().crop_gallery - - # Region class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView): """Create view for model Region""" From 1ba660b5fdac885500f45bb9e81bcf4b12f1a448 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 15 Jan 2020 14:48:50 +0300 Subject: [PATCH 038/119] add verification code to confirm email template --- project/templates/authorization/confirm_email.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/project/templates/authorization/confirm_email.html b/project/templates/authorization/confirm_email.html index 8b1332d0..cbe340dc 100644 --- a/project/templates/authorization/confirm_email.html +++ b/project/templates/authorization/confirm_email.html @@ -36,6 +36,9 @@ {% trans "Please confirm your email address to complete the registration:" %} https://{{ country_code }}.{{ domain_uri }}/registered/{{ uidb64 }}/{{ token }}/
+ {% trans "If you use the mobile app, enter the following code in the form:" %} + {{ token }} +
{% trans "Thanks for using our site!" %}

{% blocktrans %}The {{ site_name }} team{% endblocktrans %} From 63cc029ce38b83957ad2bb88907f3412883a21b6 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 15 Jan 2020 15:03:38 +0300 Subject: [PATCH 039/119] subscriptions read only --- apps/account/models.py | 10 ++++++- apps/account/serializers/back.py | 46 ++++++++++++++++++++++++++++-- apps/account/serializers/common.py | 4 +++ apps/account/views/back.py | 1 + apps/notification/admin.py | 19 +++++++++++- 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/apps/account/models.py b/apps/account/models.py index 1f050cdb..e82d5189 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -340,6 +340,14 @@ class User(AbstractUser): model='product', ).values_list('object_id', flat=True) + @property + def subscriptions(self): + result = [] + for subscription in self.subscriber.all(): + for item in subscription.active_subscriptions: + result.append(item.id) + return set(result) + class UserRole(ProjectBaseMixin): """UserRole model.""" @@ -380,4 +388,4 @@ class OldRole(models.Model): old_role = models.CharField(verbose_name=_('Old role'), max_length=512, null=True) class Meta: - unique_together = ('new_role', 'old_role') \ No newline at end of file + unique_together = ('new_role', 'old_role') diff --git a/apps/account/serializers/back.py b/apps/account/serializers/back.py index a5a0ccdb..97e6e12f 100644 --- a/apps/account/serializers/back.py +++ b/apps/account/serializers/back.py @@ -45,11 +45,21 @@ class BackUserSerializer(serializers.ModelSerializer): 'last_ip', 'last_country', 'roles', + 'subscriptions', ) extra_kwargs = { 'password': {'write_only': True}, } - read_only_fields = ('old_password', 'last_login', 'date_joined', 'city', 'locale', 'last_ip', 'last_country') + read_only_fields = ( + 'old_password', + 'last_login', + 'date_joined', + 'city', + 'locale', + 'last_ip', + 'last_country', + 'subscriptions', + ) def create(self, validated_data): user = super().create(validated_data) @@ -61,8 +71,38 @@ class BackUserSerializer(serializers.ModelSerializer): class BackDetailUserSerializer(BackUserSerializer): class Meta: model = User - exclude = ('password',) - read_only_fields = ('old_password', 'last_login', 'date_joined') + fields = ( + 'id', + 'last_country', + 'roles', + 'last_login', + 'is_superuser', + 'first_name', + 'last_name', + 'is_staff', + 'is_active', + 'date_joined', + 'username', + 'image_url', + 'cropped_image_url', + 'email', + 'unconfirmed_email', + 'email_confirmed', + 'newsletter', + 'old_id', + 'locale', + 'city', + 'last_ip', + 'groups', + 'user_permissions', + 'subscriptions', + ) + read_only_fields = ( + 'old_password', + 'last_login', + 'date_joined', + 'subscriptions', + ) def create(self, validated_data): user = super().create(validated_data) diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index d9df65e8..7929ad77 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -56,6 +56,7 @@ class UserSerializer(serializers.ModelSerializer): 'email_confirmed', 'newsletter', 'roles', + 'subscriptions', ] extra_kwargs = { 'first_name': {'required': False, 'write_only': True, }, @@ -65,6 +66,9 @@ class UserSerializer(serializers.ModelSerializer): 'cropped_image_url': {'required': False, }, 'newsletter': {'required': False, }, } + read_only_fields = ( + 'subscriptions', + ) def create(self, validated_data): user = super(UserSerializer, self).create(validated_data) diff --git a/apps/account/views/back.py b/apps/account/views/back.py index cb68cec6..49185557 100644 --- a/apps/account/views/back.py +++ b/apps/account/views/back.py @@ -27,6 +27,7 @@ class UserListView(generics.ListCreateAPIView): serializer_class = serializers.BackUserSerializer permission_classes = (permissions.IsAdminUser,) filter_backends = (DjangoFilterBackend, OrderingFilter) + filterset_fields = ( 'email_confirmed', 'is_staff', diff --git a/apps/notification/admin.py b/apps/notification/admin.py index 8c38f3f3..5c8a01ec 100644 --- a/apps/notification/admin.py +++ b/apps/notification/admin.py @@ -1,3 +1,20 @@ from django.contrib import admin +from notification import models -# Register your models here. + +@admin.register(models.SubscriptionType) +class SubscriptionTypeAdmin(admin.ModelAdmin): + """SubscriptionType admin.""" + list_display = ['index_name', 'country'] + + +@admin.register(models.Subscriber) +class SubscriberAdmin(admin.ModelAdmin): + """Subscriber admin.""" + raw_id_fields = ('user',) + + +@admin.register(models.Subscribe) +class SubscribeAdmin(admin.ModelAdmin): + """Subscribe admin.""" + raw_id_fields = ('subscriber',) From 21978639bd8b63abc3b553606bc2f05dde1cdea7 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 15 Jan 2020 16:40:29 +0300 Subject: [PATCH 040/119] possible fix for similar distilleries --- apps/establishment/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index c10be29d..24373793 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -365,10 +365,10 @@ class EstablishmentQuerySet(models.QuerySet): Return QuerySet with objects that similar to Distillery. :param distillery: Establishment instance """ - base_qs = self.similar_base(distillery).same_subtype(distillery) + base_qs = self.similar_base(distillery).same_subtype(distillery).exclude(same_subtype=False) similarity_rules = { - 'ordering': [F('same_subtype').desc(), ], - 'distinctions': ['same_subtype', ] + 'distinctions': [], + 'ordering': [], } if distillery.address and distillery.address.coordinates: base_qs = base_qs.annotate_distance(point=distillery.location) From 59c9c06d6471f4e6b273a0ca45e35ca7a2e819df Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 15 Jan 2020 16:46:03 +0300 Subject: [PATCH 041/119] fix transfer location --- apps/location/transfer_data.py | 38 +++------------- apps/transfer/management/commands/transfer.py | 2 - apps/transfer/serializers/location.py | 44 ------------------- 3 files changed, 6 insertions(+), 78 deletions(-) diff --git a/apps/location/transfer_data.py b/apps/location/transfer_data.py index ba6c535f..caf04c92 100644 --- a/apps/location/transfer_data.py +++ b/apps/location/transfer_data.py @@ -10,7 +10,7 @@ from tqdm import tqdm from account.models import Role from collection.models import Collection from gallery.models import Image -from location.models import Country, Region, City, Address, CityGallery +from location.models import Country, Region, City, Address from main.models import AwardType from news.models import News from review.models import Review @@ -218,29 +218,6 @@ def migrate_city_map_situation(get_exists_cities=False): pprint(f"City info serializer errors: {serialized_data.errors}") -def migrate_city_photos(): - queryset = transfer_models.CityPhotos.objects.raw("""SELECT city_photos.id, city_photos.city_id, city_photos.attachment_file_name - FROM city_photos WHERE - city_photos.attachment_file_name IS NOT NULL AND - city_id IN( - SELECT cities.id - FROM cities WHERE - region_code IS NOT NULL AND - region_code != "" AND - country_code_2 IS NOT NULL AND - country_code_2 != "" - ) - """) - - queryset = [vars(query) for query in queryset] - - serialized_data = location_serializers.CityGallerySerializer(data=queryset, many=True) - if serialized_data.is_valid(): - serialized_data.save() - else: - pprint(f"Address serializer errors: {serialized_data.errors}") - - # Update location models with ruby library # Utils functions defined before transfer functions def get_ruby_socket(params): @@ -554,7 +531,7 @@ def remove_old_records(): clean_old_region_records(Region, {"mysql_ids__isnull": True}) -def transfer_city_gallery(): +def transfer_city_photos(): created_counter = 0 cities_not_exists = {} cities_has_same_image = 0 @@ -757,8 +734,8 @@ def setup_clean_db(): print('update_flags') update_flags() - print('transfer_city_gallery') - transfer_city_gallery() + print('transfer_city_photos') + transfer_city_photos() def set_unused_regions(): @@ -811,9 +788,6 @@ data_types = { "update_city_info": [ migrate_city_map_situation ], - "migrate_city_gallery": [ - migrate_city_photos - ], "fix_location": [ add_fake_country, fix_location_models, @@ -821,8 +795,8 @@ data_types = { "remove_old_locations": [ remove_old_records ], - "fill_city_gallery": [ - transfer_city_gallery, + "migrate_city_photos": [ + transfer_city_photos, ], "add_fake_country": [ add_fake_country, diff --git a/apps/transfer/management/commands/transfer.py b/apps/transfer/management/commands/transfer.py index 7dc76d45..2f747300 100644 --- a/apps/transfer/management/commands/transfer.py +++ b/apps/transfer/management/commands/transfer.py @@ -40,7 +40,6 @@ class Command(BaseCommand): 'product_review', 'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1 'purchased_plaques', # №6 - перенос купленных тарелок - 'fill_city_gallery', # №3 - перенос галереи городов 'guides', 'guide_filters', 'guide_element_sections', @@ -51,7 +50,6 @@ class Command(BaseCommand): 'guide_element_label_photo', 'guide_complete', 'update_city_info', - 'migrate_city_gallery', 'fix_location', 'remove_old_locations', 'add_fake_country', diff --git a/apps/transfer/serializers/location.py b/apps/transfer/serializers/location.py index 184a0bf5..b2130ad5 100644 --- a/apps/transfer/serializers/location.py +++ b/apps/transfer/serializers/location.py @@ -431,50 +431,6 @@ class CityMapCorrectSerializer(CityMapSerializer): city.save() -class CityGallerySerializer(serializers.ModelSerializer): - id = serializers.IntegerField() - city_id = serializers.IntegerField() - attachment_file_name = serializers.CharField() - - class Meta: - model = models.CityGallery - fields = ("id", "city_id", "attachment_file_name") - - def validate(self, data): - data = self.set_old_id(data) - data = self.set_gallery(data) - data = self.set_city(data) - return data - - def create(self, validated_data): - return models.CityGallery.objects.create(**validated_data) - - def set_old_id(self, data): - data['old_id'] = data.pop('id') - return data - - def set_gallery(self, data): - link_prefix = "city_photos/00baf486523f62cdf131fa1b19c5df2bf21fc9f8/" - try: - data['image'] = Image.objects.create( - image=f"{link_prefix}{data['attachment_file_name']}" - ) - except Exception as e: - raise ValueError(f"Cannot create image with {data}: {e}") - del(data['attachment_file_name']) - return data - - def set_city(self, data): - try: - data['city'] = models.City.objects.get(old_id=data.pop('city_id')) - except models.City.DoesNotExist as e: - raise ValueError(f"Cannot get city with {data}: {e}") - except MultipleObjectsReturned as e: - raise ValueError(f"Multiple cities find with {data}: {e}") - - return data - - class CepageWineRegionSerializer(TransferSerializerMixin): CATEGORY_LABEL = 'Cepage' From 0607a56739008e80911d1d97ec4d8655dddd52d5 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 15 Jan 2020 16:46:53 +0300 Subject: [PATCH 042/119] Revert "possible fix for similar distilleries" This reverts commit 2197863 --- apps/establishment/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 24373793..c10be29d 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -365,10 +365,10 @@ class EstablishmentQuerySet(models.QuerySet): Return QuerySet with objects that similar to Distillery. :param distillery: Establishment instance """ - base_qs = self.similar_base(distillery).same_subtype(distillery).exclude(same_subtype=False) + base_qs = self.similar_base(distillery).same_subtype(distillery) similarity_rules = { - 'distinctions': [], - 'ordering': [], + 'ordering': [F('same_subtype').desc(), ], + 'distinctions': ['same_subtype', ] } if distillery.address and distillery.address.coordinates: base_qs = base_qs.annotate_distance(point=distillery.location) From 129201d3a99cdb07cd027ea8e2a0f06c18b75200 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 15 Jan 2020 16:51:42 +0300 Subject: [PATCH 043/119] add slug for backoffice establishments --- apps/establishment/serializers/back.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 893e9439..29fdd671 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -115,6 +115,7 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer): model = models.Establishment fields = [ 'id', + 'slug', 'name', 'website', 'phones', From c0273773bc13b901ae853511aa6cee5a56c5b69e Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 15 Jan 2020 17:00:01 +0300 Subject: [PATCH 044/119] refactored a little similarity rules --- apps/establishment/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index c10be29d..b1400870 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -380,18 +380,18 @@ class EstablishmentQuerySet(models.QuerySet): .order_by(*similarity_rules['ordering']) \ .distinct(*similarity_rules['distinctions'], 'id') - def similar_food_producers(self, distillery): + def similar_food_producers(self, food_producer): """ Return QuerySet with objects that similar to Food Producer. - :param distillery: Establishment instance + :param food_producer: Establishment instance """ - base_qs = self.similar_base(distillery).same_subtype(distillery) + base_qs = self.similar_base(food_producer).same_subtype(food_producer) similarity_rules = { 'ordering': [F('same_subtype').desc(), ], 'distinctions': ['same_subtype', ] } - if distillery.address and distillery.address.coordinates: - base_qs = base_qs.annotate_distance(point=distillery.location) + if food_producer.address and food_producer.address.coordinates: + base_qs = base_qs.annotate_distance(point=food_producer.location) similarity_rules['ordering'].append(F('distance').asc()) similarity_rules['distinctions'].append('distance') From f15ac471022ab87a0fc227adbe1e09384e48a028 Mon Sep 17 00:00:00 2001 From: alex Date: Wed, 15 Jan 2020 17:06:09 +0300 Subject: [PATCH 045/119] subscription_type list for user --- apps/account/models.py | 2 +- apps/account/serializers/back.py | 35 +++++++++++++++++++-- apps/account/serializers/common.py | 49 +++++++++++++++++++++++++++--- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/apps/account/models.py b/apps/account/models.py index e82d5189..c4d65d77 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -341,7 +341,7 @@ class User(AbstractUser): ).values_list('object_id', flat=True) @property - def subscriptions(self): + def subscription_types(self): result = [] for subscription in self.subscriber.all(): for item in subscription.active_subscriptions: diff --git a/apps/account/serializers/back.py b/apps/account/serializers/back.py index 97e6e12f..deac6f6c 100644 --- a/apps/account/serializers/back.py +++ b/apps/account/serializers/back.py @@ -3,7 +3,7 @@ from rest_framework import serializers from account import models from account.models import User -from account.serializers import RoleBaseSerializer +from account.serializers import RoleBaseSerializer, subscriptions_handler from main.models import SiteSettings @@ -20,6 +20,14 @@ class _SiteSettingsSerializer(serializers.ModelSerializer): class BackUserSerializer(serializers.ModelSerializer): last_country = _SiteSettingsSerializer(read_only=True) roles = RoleBaseSerializer(many=True, read_only=True) + subscriptions = serializers.ListField( + source='subscription_types', + allow_null=True, + allow_empty=True, + child=serializers.IntegerField(min_value=1), + required=False, + help_text='list of subscription_types id', + ) class Meta: model = User @@ -58,13 +66,18 @@ class BackUserSerializer(serializers.ModelSerializer): 'locale', 'last_ip', 'last_country', - 'subscriptions', ) def create(self, validated_data): + subscriptions_list = [] + if 'subscription_types' in validated_data: + subscriptions_list = validated_data.pop('subscription_types') + user = super().create(validated_data) user.set_password(validated_data['password']) user.save() + + subscriptions_handler(subscriptions_list, user) return user @@ -101,15 +114,31 @@ class BackDetailUserSerializer(BackUserSerializer): 'old_password', 'last_login', 'date_joined', - 'subscriptions', + 'last_ip', + 'last_country', ) def create(self, validated_data): + subscriptions_list = [] + if 'subscription_types' in validated_data: + subscriptions_list = validated_data.pop('subscription_types') + user = super().create(validated_data) user.set_password(validated_data['password']) user.save() + + subscriptions_handler(subscriptions_list, user) return user + def update(self, instance, validated_data): + subscriptions_list = [] + if 'subscription_types' in validated_data: + subscriptions_list = validated_data.pop('subscription_types') + + instance = super().update(instance, validated_data) + subscriptions_handler(subscriptions_list, instance) + return instance + class UserRoleSerializer(serializers.ModelSerializer): class Meta: diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index 7929ad77..02d75950 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -9,9 +9,34 @@ from rest_framework import validators as rest_validators from account import models, tasks from main.serializers.common import NavigationBarPermissionBaseSerializer -from notification.models import Subscriber +from notification.models import Subscribe, Subscriber from utils import exceptions as utils_exceptions from utils import methods as utils_methods +from utils.methods import generate_string_code + + +def subscriptions_handler(subscriptions_list, user): + """ + create or update subscriptions for user + """ + Subscribe.objects.filter(subscriber__user=user).delete() + subscriber, _ = Subscriber.objects.get_or_create( + email=user.email, + defaults={ + 'user': user, + 'email': user.email, + 'ip_address': user.last_ip, + 'country_code': user.last_country.country.code if user.last_country else None, + 'locale': user.locale, + 'update_code': generate_string_code(), + } + ) + + for subscription in subscriptions_list: + Subscribe.objects.create( + subscriber=subscriber, + subscription_type_id=subscription, + ) class RoleBaseSerializer(serializers.ModelSerializer): @@ -42,6 +67,14 @@ class UserSerializer(serializers.ModelSerializer): validators=(rest_validators.UniqueValidator(queryset=models.User.objects.all()),), required=False) roles = RoleBaseSerializer(many=True, read_only=True) + subscriptions = serializers.ListField( + source='subscription_types', + allow_null=True, + allow_empty=True, + child=serializers.IntegerField(min_value=1), + required=False, + help_text='list of subscription_types id', + ) class Meta: model = models.User @@ -66,14 +99,16 @@ class UserSerializer(serializers.ModelSerializer): 'cropped_image_url': {'required': False, }, 'newsletter': {'required': False, }, } - read_only_fields = ( - 'subscriptions', - ) def create(self, validated_data): + subscriptions_list = [] + if 'subscription_types' in validated_data: + subscriptions_list = validated_data.pop('subscription_types') + user = super(UserSerializer, self).create(validated_data) validated_data['user'] = user Subscriber.objects.make_subscriber(**validated_data) + subscriptions_handler(subscriptions_list, user) return user def validate_email(self, value): @@ -91,6 +126,10 @@ class UserSerializer(serializers.ModelSerializer): def update(self, instance, validated_data): """Override update method""" + subscriptions_list = [] + if 'subscription_types' in validated_data: + subscriptions_list = validated_data.pop('subscription_types') + old_email = instance.email instance = super().update(instance, validated_data) if 'email' in validated_data: @@ -109,6 +148,8 @@ class UserSerializer(serializers.ModelSerializer): user_id=instance.id, country_code=self.context.get('request').country_code, emails=[validated_data['email'], ]) + + subscriptions_handler(subscriptions_list, instance) return instance From 59f7f8d2d41b688c38edd1a24aede3e2e9054254 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 15 Jan 2020 17:49:57 +0300 Subject: [PATCH 046/119] distillery_type --- apps/establishment/models.py | 9 ++++----- apps/establishment/serializers/common.py | 8 ++++++-- apps/establishment/views/web.py | 3 ++- apps/search_indexes/documents/establishment.py | 6 +++--- apps/search_indexes/serializers.py | 4 ++-- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index b1400870..d54b7a61 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -784,6 +784,10 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, def artisan_category_indexing(self): return self.tags.filter(category__index_name='shop_category') + @property + def distillery_type_indexing(self): + return self.tags.filter(category__index_name='distillery_type') + @property def last_comment(self): if hasattr(self, 'comments_prefetched') and len(self.comments_prefetched): @@ -854,11 +858,6 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, metadata.append(category_tags) return metadata - @property - def distillery_types(self): - """Tags from tag category - distillery_type.""" - return self.tags.filter(category__index_name='distillery_type') - class EstablishmentNoteQuerySet(models.QuerySet): """QuerySet for model EstablishmentNote.""" diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index eef4dbe7..1d0317f4 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -327,7 +327,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): read_only=True) tz = serializers.CharField(read_only=True, source='timezone_as_str') new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True) - distillery_types = TagBaseSerializer(read_only=True, many=True, allow_null=True) + distillery_type = TagBaseSerializer(read_only=True, many=True, allow_null=True) class Meta: """Meta class.""" @@ -353,7 +353,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): 'new_image', 'tz', 'wine_regions', - 'distillery_types', + 'distillery_type', ] @@ -365,6 +365,7 @@ class EstablishmentListRetrieveSerializer(EstablishmentBaseSerializer): 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) + distillery_type = TagBaseSerializer(read_only=True, many=True, allow_null=True) class Meta(EstablishmentBaseSerializer.Meta): """Meta class.""" @@ -374,6 +375,7 @@ class EstablishmentListRetrieveSerializer(EstablishmentBaseSerializer): 'restaurant_category', 'restaurant_cuisine', 'artisan_category', + 'distillery_type', ] @@ -467,6 +469,7 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer): artisan_category = TagBaseSerializer(many=True, allow_null=True, read_only=True) restaurant_category = TagBaseSerializer(many=True, allow_null=True, read_only=True) restaurant_cuisine = TagBaseSerializer(many=True, allow_null=True, read_only=True) + distillery_type = TagBaseSerializer(many=True, allow_null=True, read_only=True) class Meta(EstablishmentBaseSerializer.Meta): fields = EstablishmentBaseSerializer.Meta.fields + [ @@ -475,6 +478,7 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer): 'artisan_category', 'restaurant_category', 'restaurant_cuisine', + 'distillery_type', ] diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index e2c8fa5e..ec244583 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -38,7 +38,8 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView): .with_extended_address_related().with_currency_related() \ .with_certain_tag_category_related('category', 'restaurant_category') \ .with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \ - .with_certain_tag_category_related('shop_category', 'artisan_category') + .with_certain_tag_category_related('shop_category', 'artisan_category') \ + .with_certain_tag_category_related('distillery_type', 'distillery_type') class EstablishmentSimilarView(EstablishmentListView): diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 20185b9c..0dcd2f17 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -77,15 +77,15 @@ class EstablishmentDocument(Document): 'value': fields.KeywordField(), }, multi=True, attr='artisan_category_indexing') - visible_tags = fields.ObjectField( + distillery_type = fields.ObjectField( properties={ 'id': fields.IntegerField(attr='id'), 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), 'value': fields.KeywordField(), }, - multi=True) - distillery_types = fields.ObjectField( + multi=True, attr='distillery_type_indexing') + visible_tags = fields.ObjectField( properties={ 'id': fields.IntegerField(attr='id'), 'label': fields.ObjectField(attr='label_indexing', diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index b3c55ab1..b909fb1b 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -277,7 +277,7 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): tags = TagsDocumentSerializer(many=True, source='visible_tags') restaurant_category = TagsDocumentSerializer(many=True, allow_null=True) restaurant_cuisine = TagsDocumentSerializer(many=True, allow_null=True) - distillery_types = TagsDocumentSerializer(many=True, allow_null=True) + distillery_type = TagsDocumentSerializer(many=True, allow_null=True) artisan_category = TagsDocumentSerializer(many=True, allow_null=True) schedule = ScheduleDocumentSerializer(many=True, allow_null=True) wine_origins = WineOriginSerializer(many=True) @@ -311,7 +311,7 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): # 'collections', 'type', 'subtypes', - 'distillery_types', + 'distillery_type', ) From a468f264e08a9e3160f13a5fdce91b1e2d6bb99b Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 15 Jan 2020 17:50:24 +0300 Subject: [PATCH 047/119] fix similarity rules --- apps/establishment/models.py | 24 ++++++++++++++---------- apps/product/models.py | 4 ++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index d54b7a61..5eab194e 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -296,7 +296,7 @@ class EstablishmentQuerySet(models.QuerySet): .order_by('mark_similarity') \ .distinct('mark_similarity', 'id') - def same_subtype(self, establishment): + def annotate_same_subtype(self, establishment): """Annotate flag same subtype.""" return self.annotate(same_subtype=Case( models.When( @@ -312,10 +312,10 @@ class EstablishmentQuerySet(models.QuerySet): Return QuerySet with objects that similar to Artisan/Producer(s). :param establishment: Establishment instance """ - base_qs = self.similar_base(establishment).same_subtype(establishment) + base_qs = self.similar_base(establishment).annotate_same_subtype(establishment) similarity_rules = { - 'ordering': [F('same_subtype').desc(), ], - 'distinctions': ['same_subtype', ] + 'ordering': [F('annotate_same_subtype').desc(), ], + 'distinctions': ['annotate_same_subtype', ] } if establishment.address and establishment.address.coordinates: base_qs = base_qs.annotate_distance(point=establishment.location) @@ -365,10 +365,12 @@ class EstablishmentQuerySet(models.QuerySet): Return QuerySet with objects that similar to Distillery. :param distillery: Establishment instance """ - base_qs = self.similar_base(distillery).same_subtype(distillery) + base_qs = self.similar_base(distillery).annotate_same_subtype(distillery).filter( + same_subtype=True + ) similarity_rules = { - 'ordering': [F('same_subtype').desc(), ], - 'distinctions': ['same_subtype', ] + 'ordering': [], + 'distinctions': [] } if distillery.address and distillery.address.coordinates: base_qs = base_qs.annotate_distance(point=distillery.location) @@ -385,10 +387,12 @@ class EstablishmentQuerySet(models.QuerySet): Return QuerySet with objects that similar to Food Producer. :param food_producer: Establishment instance """ - base_qs = self.similar_base(food_producer).same_subtype(food_producer) + base_qs = self.similar_base(food_producer).annotate_same_subtype(food_producer).filter( + same_subtype=True + ) similarity_rules = { - 'ordering': [F('same_subtype').desc(), ], - 'distinctions': ['same_subtype', ] + 'ordering': [], + 'distinctions': [] } if food_producer.address and food_producer.address.coordinates: base_qs = base_qs.annotate_distance(point=food_producer.location) diff --git a/apps/product/models.py b/apps/product/models.py index d31e2477..8be6ddd4 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -183,7 +183,7 @@ class ProductQuerySet(models.QuerySet): """Return objects with geo location.""" return self.filter(establishment__address__coordinates__isnull=False) - def same_subtype(self, product): + def annotate_same_subtype(self, product): """Annotate flag same subtype.""" return self.annotate(same_subtype=Case( models.When( @@ -222,7 +222,7 @@ class ProductQuerySet(models.QuerySet): similarity_rules['ordering'].append(F('distance').asc()) similarity_rules['distinction'].append('distance') return self.similar_base(product) \ - .same_subtype(product) \ + .annotate_same_subtype(product) \ .order_by(*similarity_rules['ordering']) \ .distinct(*similarity_rules['distinction'], 'id') From 9b40d8e48e6ba235961da082be3125cbc1648681 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 15 Jan 2020 18:02:19 +0300 Subject: [PATCH 048/119] distillery_type #2 --- apps/favorites/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/favorites/views.py b/apps/favorites/views.py index 4faa3e07..1e7a12b3 100644 --- a/apps/favorites/views.py +++ b/apps/favorites/views.py @@ -32,7 +32,8 @@ class FavoritesEstablishmentListView(generics.ListAPIView): .order_by('-favorites').with_base_related() \ .with_certain_tag_category_related('category', 'restaurant_category') \ .with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \ - .with_certain_tag_category_related('shop_category', 'artisan_category') + .with_certain_tag_category_related('shop_category', 'artisan_category') \ + .with_certain_tag_category_related('distillery_type', 'distillery_type') class FavoritesProductListView(generics.ListAPIView): From fab351d80089aaf2d84f2712786a8c20e592bbae Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 15 Jan 2020 18:21:21 +0300 Subject: [PATCH 049/119] distillery_type #3 --- apps/establishment/views/web.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index ec244583..a84c5647 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -68,7 +68,8 @@ class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView serializer_class = serializers.EstablishmentDetailSerializer def get_queryset(self): - return super().get_queryset().with_extended_related() + return super().get_queryset().with_extended_related() \ + .with_certain_tag_category_related('distillery_type', 'distillery_type') class EstablishmentMobileRetrieveView(EstablishmentRetrieveView): From 7a7d7a70b08b661c695b39e89db60e15af901d51 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 15 Jan 2020 19:15:32 +0300 Subject: [PATCH 050/119] fix naming issue --- apps/establishment/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 5eab194e..1d693219 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -314,8 +314,8 @@ class EstablishmentQuerySet(models.QuerySet): """ base_qs = self.similar_base(establishment).annotate_same_subtype(establishment) similarity_rules = { - 'ordering': [F('annotate_same_subtype').desc(), ], - 'distinctions': ['annotate_same_subtype', ] + 'ordering': [F('same_subtype').desc(), ], + 'distinctions': ['same_subtype', ] } if establishment.address and establishment.address.coordinates: base_qs = base_qs.annotate_distance(point=establishment.location) From 4b45cab9761d476ae0a22d206bd5ce91ac987dca Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 15 Jan 2020 20:49:33 +0300 Subject: [PATCH 051/119] employees trigram search for back-office --- apps/establishment/filters.py | 11 ++++++++++ .../migrations/0072_auto_20200115_1702.py | 16 ++++++++++++++ .../migrations/0073_auto_20200115_1710.py | 22 +++++++++++++++++++ apps/establishment/models.py | 16 ++++++++++++++ apps/establishment/urls/back.py | 1 + apps/establishment/views/back.py | 11 +++++++++- 6 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 apps/establishment/migrations/0072_auto_20200115_1702.py create mode 100644 apps/establishment/migrations/0073_auto_20200115_1710.py diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index 6dd70222..58826e19 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -1,6 +1,8 @@ """Establishment app filters.""" from django.core.validators import EMPTY_VALUES from django_filters import rest_framework as filters +from rest_framework.serializers import ValidationError +from django.utils.translation import ugettext_lazy as _ from establishment import models @@ -79,3 +81,12 @@ class EmployeeBackFilter(filters.FilterSet): if value not in EMPTY_VALUES: return queryset.search_by_name_or_last_name(value) return queryset + + +class EmployeeBackSearchFilter(EmployeeBackFilter): + def search_by_name_or_last_name(self, queryset, name, value): + if value not in EMPTY_VALUES: + if len(value) < 3: + raise ValidationError({'detail': _('Type at least 3 characters to search please.')}) + return queryset.trigram_search(value) + return queryset diff --git a/apps/establishment/migrations/0072_auto_20200115_1702.py b/apps/establishment/migrations/0072_auto_20200115_1702.py new file mode 100644 index 00000000..4ea732fb --- /dev/null +++ b/apps/establishment/migrations/0072_auto_20200115_1702.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2.7 on 2020-01-15 17:02 + +from django.db import migrations +from django.contrib.postgres.operations import TrigramExtension, BtreeGinExtension + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0071_auto_20200110_1055'), + ] + + operations = [ + TrigramExtension(), + BtreeGinExtension(), + ] diff --git a/apps/establishment/migrations/0073_auto_20200115_1710.py b/apps/establishment/migrations/0073_auto_20200115_1710.py new file mode 100644 index 00000000..375c563e --- /dev/null +++ b/apps/establishment/migrations/0073_auto_20200115_1710.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.7 on 2020-01-15 17:10 + +import django.contrib.postgres.indexes +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0072_auto_20200115_1702'), + ] + + operations = [ + migrations.AddIndex( + model_name='employee', + index=django.contrib.postgres.indexes.GinIndex(fields=['name'], name='establishme_name_39fda6_gin'), + ), + migrations.AddIndex( + model_name='employee', + index=django.contrib.postgres.indexes.GinIndex(fields=['last_name'], name='establishme_last_na_3c53de_gin'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 1d693219..325d9d8d 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -11,6 +11,8 @@ 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 +from django.contrib.postgres.indexes import GinIndex from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models @@ -977,6 +979,13 @@ class EmployeeQuerySet(models.QuerySet): ] return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters])) + def trigram_search(self, search_value: str): + """Search with mistakes by name or last name.""" + return self.annotate( + name_distance=TrigramDistance('name', search_value.lower()), + last_name_distance=TrigramDistance('last_name', search_value.lower()), + ).filter(Q(name_distance__lte=0.7) | Q(last_name_distance__lte=0.7)).order_by('name_distance') + def search_by_name_or_last_name(self, value): """Search by name or last_name.""" return self._generic_search(value, ['name', 'last_name']) @@ -987,6 +996,9 @@ class EmployeeQuerySet(models.QuerySet): queryset=EstablishmentEmployee.objects.actual() )).all().distinct() + def with_extended_related(self): + return self.prefetch_related('establishments') + class Employee(BaseAttributes): """Employee model.""" @@ -1030,6 +1042,10 @@ class Employee(BaseAttributes): verbose_name = _('Employee') verbose_name_plural = _('Employees') + indexes = [ + GinIndex(fields=('name',)), + GinIndex(fields=('last_name',)) + ] class EstablishmentScheduleQuerySet(models.QuerySet): diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index b2a30917..10351fe5 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -45,6 +45,7 @@ urlpatterns = [ path('/employees/', views.EstablishmentEmployeeListView.as_view(), name='establishment-employees'), path('employees/', views.EmployeeListCreateView.as_view(), name='employees'), + path('employees/search/', views.EmployeesListSearchViews.as_view(), name='employees-search'), path('employees//', views.EmployeeRUDView.as_view(), name='employees-rud'), path('/employee//position/', views.EstablishmentEmployeeCreateView.as_view(), diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 19a75fd5..bc22f5ce 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -2,7 +2,7 @@ from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404 from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import generics, permissions, status +from rest_framework import generics, permissions, status, filters as rest_filters from account.models import User from establishment import filters, models, serializers @@ -174,6 +174,15 @@ class EmployeeListCreateView(generics.ListCreateAPIView): queryset = models.Employee.objects.all() +class EmployeesListSearchViews(generics.ListAPIView): + """Employee search view""" + pagination_class = None + permission_classes = (permissions.AllowAny,) + queryset = models.Employee.objects.all() + filter_class = filters.EmployeeBackSearchFilter + serializer_class = serializers.EmployeeBackSerializers + + class EstablishmentEmployeeListView(generics.ListCreateAPIView): """Establishment emplyoees list view.""" permission_classes = (permissions.AllowAny,) From 6461ffda4dcaea84431fe797e769fd4385f2f97f Mon Sep 17 00:00:00 2001 From: dormantman Date: Wed, 15 Jan 2020 21:45:29 +0300 Subject: [PATCH 052/119] Added short to label_translated --- apps/tag/views.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/tag/views.py b/apps/tag/views.py index 7467cbb6..d13edd75 100644 --- a/apps/tag/views.py +++ b/apps/tag/views.py @@ -156,8 +156,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): "filters": [{ "id": weekday, "index_name": week_days[weekday].lower(), - "label_translated": week_days[weekday], - "label_translated_short": short_week_days[weekday], + "label_translated": short_week_days[weekday], } for weekday in range(7)] } result_list.append(works_noon) @@ -172,8 +171,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): "filters": [{ "id": weekday, "index_name": week_days[weekday].lower(), - "label_translated": week_days[weekday], - "label_translated_short": short_week_days[weekday], + "label_translated": short_week_days[weekday], } for weekday in range(7)] } result_list.append(works_evening) @@ -196,8 +194,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): "filters": [{ "id": weekday, "index_name": week_days[weekday].lower(), - "label_translated": week_days[weekday], - "label_translated_short": short_week_days[weekday], + "label_translated": short_week_days[weekday], } for weekday in range(7)] } result_list.append(works_at_weekday) From f86357e0417308e208871205d492500eb9d6743e Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 15 Jan 2020 22:31:17 +0300 Subject: [PATCH 053/119] fix comments --- .../commands/add_status_to_comments.py | 18 +++++++++ .../comment/migrations/0008_comment_status.py | 18 +++++++++ apps/comment/models.py | 38 ++++++++++++++++++- apps/comment/serializers/common.py | 23 ++++------- apps/comment/transfer_data.py | 1 + apps/establishment/serializers/common.py | 16 +++++--- apps/establishment/views/web.py | 8 ++-- apps/product/serializers/common.py | 16 +++++--- apps/product/views/common.py | 10 ++--- apps/transfer/serializers/comments.py | 16 +++++++- 10 files changed, 124 insertions(+), 40 deletions(-) create mode 100644 apps/comment/management/commands/add_status_to_comments.py create mode 100644 apps/comment/migrations/0008_comment_status.py diff --git a/apps/comment/management/commands/add_status_to_comments.py b/apps/comment/management/commands/add_status_to_comments.py new file mode 100644 index 00000000..932e80df --- /dev/null +++ b/apps/comment/management/commands/add_status_to_comments.py @@ -0,0 +1,18 @@ +from django.core.management.base import BaseCommand +from comment.models import Comment +from tqdm import tqdm + + +class Command(BaseCommand): + help = 'Add status to comments from is_publish_ flag.' + + def handle(self, *args, **kwargs): + to_update = [] + + for comment in tqdm(Comment.objects.all()): + if hasattr(comment, 'is_publish') and hasattr(comment, 'status'): + comment.status = Comment.PUBLISHED if comment.is_publish else Comment.WAITING + to_update.append(comment) + + Comment.objects.bulk_update(to_update, ('status', )) + self.stdout.write(self.style.WARNING(f'Updated {len(to_update)} objects.')) diff --git a/apps/comment/migrations/0008_comment_status.py b/apps/comment/migrations/0008_comment_status.py new file mode 100644 index 00000000..8441d81e --- /dev/null +++ b/apps/comment/migrations/0008_comment_status.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2020-01-15 13:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('comment', '0007_comment_site'), + ] + + operations = [ + migrations.AddField( + model_name='comment', + name='status', + field=models.PositiveSmallIntegerField(choices=[(0, 'Waiting'), (1, 'Published'), (2, 'Rejected'), (3, 'Deleted')], default=0, verbose_name='status'), + ), + ] diff --git a/apps/comment/models.py b/apps/comment/models.py index fa27d3f9..7844b29c 100644 --- a/apps/comment/models.py +++ b/apps/comment/models.py @@ -16,6 +16,10 @@ class CommentQuerySet(ContentTypeQuerySetMixin): """Return comments by author""" return self.filter(user=user) + def published(self): + """Return only published comments.""" + return self.filter(status=self.model.PUBLISHED) + def annotate_is_mine_status(self, user): """Annotate belonging status""" return self.annotate(is_mine=models.Case( @@ -27,16 +31,48 @@ class CommentQuerySet(ContentTypeQuerySetMixin): output_field=models.BooleanField() )) + def public(self, user): + """ + Return QuerySet by rules: + 1 With status PUBLISHED + 2 With status WAITING if comment author is + """ + qs = self.published() + if isinstance(user, User): + waiting_ids = list( + self.filter(status=self.model.WAITING, user=user) + .values_list('id', flat=True)) + published_ids = list(qs.values_list('id', flat=True)) + waiting_ids.extend(published_ids) + qs = self.filter(id__in=tuple(waiting_ids)) + return qs + class Comment(ProjectBaseMixin): """Comment model.""" + + WAITING = 0 + PUBLISHED = 1 + REJECTED = 2 + DELETED = 3 + + STATUSES = ( + (WAITING, _('Waiting')), + (PUBLISHED, _('Published')), + (REJECTED, _('Rejected')), + (DELETED, _('Deleted')), + ) + text = models.TextField(verbose_name=_('Comment text')) mark = models.PositiveIntegerField(blank=True, null=True, default=None, verbose_name=_('Mark')) user = models.ForeignKey('account.User', related_name='comments', on_delete=models.CASCADE, verbose_name=_('User')) old_id = models.IntegerField(null=True, blank=True, default=None) is_publish = models.BooleanField(default=False, verbose_name=_('Publish status')) + status = models.PositiveSmallIntegerField(choices=STATUSES, + default=WAITING, + verbose_name=_('status')) site = models.ForeignKey('main.SiteSettings', blank=True, null=True, - on_delete=models.SET_NULL, verbose_name=_('site')) + on_delete=models.SET_NULL, verbose_name=_('site')) content_type = models.ForeignKey(generic.ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey('content_type', 'object_id') diff --git a/apps/comment/serializers/common.py b/apps/comment/serializers/common.py index 8e35d3dc..1060f47f 100644 --- a/apps/comment/serializers/common.py +++ b/apps/comment/serializers/common.py @@ -4,13 +4,15 @@ from rest_framework import serializers from comment.models import Comment -class CommentSerializer(serializers.ModelSerializer): +class CommentBaseSerializer(serializers.ModelSerializer): """Comment serializer""" nickname = serializers.CharField(read_only=True, source='user.username') is_mine = serializers.BooleanField(read_only=True) profile_pic = serializers.URLField(read_only=True, source='user.cropped_image_url') + status_display = serializers.CharField(read_only=True, + source='get_status_display') class Meta: """Serializer for model Comment""" @@ -23,19 +25,10 @@ class CommentSerializer(serializers.ModelSerializer): 'text', 'mark', 'nickname', - 'profile_pic' - ] - - -class CommentRUDSerializer(CommentSerializer): - """Retrieve/Update/Destroy comment serializer.""" - class Meta(CommentSerializer.Meta): - """Meta class.""" - fields = [ - 'id', - 'created', - 'text', - 'mark', - 'nickname', 'profile_pic', + 'status', + 'status_display', ] + extra_kwargs = { + 'status': {'read_only': True}, + } diff --git a/apps/comment/transfer_data.py b/apps/comment/transfer_data.py index c9d07585..a03cd163 100644 --- a/apps/comment/transfer_data.py +++ b/apps/comment/transfer_data.py @@ -20,6 +20,7 @@ def transfer_comments(): 'mark', 'establishment_id', 'account_id', + 'state', ) serialized_data = CommentSerializer(data=list(queryset.values()), many=True) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 1d0317f4..9ce576e5 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -450,7 +450,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer): class MobileEstablishmentDetailSerializer(EstablishmentDetailSerializer): """Serializer for Establishment model for mobiles.""" - last_comment = comment_serializers.CommentRUDSerializer(allow_null=True) + last_comment = comment_serializers.CommentBaseSerializer(allow_null=True) class Meta(EstablishmentDetailSerializer.Meta): """Meta class.""" @@ -482,13 +482,11 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer): ] -class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer): +class EstablishmentCommentBaseSerializer(comment_serializers.CommentBaseSerializer): """Create comment serializer""" - mark = serializers.IntegerField() - class Meta: + class Meta(comment_serializers.CommentBaseSerializer.Meta): """Serializer for model Comment""" - model = comment_models.Comment fields = [ 'id', 'created', @@ -496,8 +494,14 @@ class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer 'mark', 'nickname', 'profile_pic', + 'status', + 'status_display', ] + +class EstablishmentCommentCreateSerializer(EstablishmentCommentBaseSerializer): + """Extended EstablishmentCommentBaseSerializer.""" + def validate(self, attrs): """Override validate method""" # Check establishment object @@ -517,7 +521,7 @@ class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer return super().create(validated_data) -class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer): +class EstablishmentCommentRUDSerializer(comment_serializers.CommentBaseSerializer): """Retrieve/Update/Destroy comment serializer.""" class Meta: diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index a84c5647..6ce8428b 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -5,7 +5,6 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions from comment import models as comment_models -from comment.serializers import CommentRUDSerializer from establishment import filters, models, serializers from main import methods from utils.pagination import PortionPagination @@ -188,18 +187,17 @@ class EstablishmentCommentListView(generics.ListAPIView): """View for return list of establishment comments.""" permission_classes = (permissions.AllowAny,) - serializer_class = serializers.EstablishmentCommentCreateSerializer + serializer_class = serializers.EstablishmentCommentBaseSerializer def get_queryset(self): """Override get_queryset method""" - establishment = get_object_or_404(models.Establishment, slug=self.kwargs['slug']) - return establishment.comments.order_by('-created') + return establishment.comments.public(self.request.user).order_by('-created') class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView): """View for retrieve/update/destroy establishment comment.""" - serializer_class = CommentRUDSerializer + serializer_class = serializers.EstablishmentCommentBaseSerializer queryset = models.Establishment.objects.all() def get_object(self): diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index e85bebe9..63766f69 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from comment.models import Comment -from comment.serializers import CommentSerializer +from comment.serializers import CommentBaseSerializer from establishment.serializers import EstablishmentProductShortSerializer from establishment.serializers.common import _EstablishmentAddressShortSerializer from location.serializers import WineOriginRegionBaseSerializer,\ @@ -200,13 +200,11 @@ class ProductFavoritesCreateSerializer(FavoritesCreateSerializer): return super().create(validated_data) -class ProductCommentCreateSerializer(CommentSerializer): - """Create comment serializer""" - mark = serializers.IntegerField() +class ProductCommentBaseSerializer(CommentBaseSerializer): + """Create comment serializer.""" - class Meta: + class Meta(CommentBaseSerializer.Meta): """Serializer for model Comment""" - model = Comment fields = [ 'id', 'created', @@ -214,8 +212,14 @@ class ProductCommentCreateSerializer(CommentSerializer): 'mark', 'nickname', 'profile_pic', + 'status', + 'status_display', ] + +class ProductCommentCreateSerializer(ProductCommentBaseSerializer): + """Serializer for creating comments for product.""" + def validate(self, attrs): """Override validate method""" # Check product object diff --git a/apps/product/views/common.py b/apps/product/views/common.py index 8b0567b2..802d04bf 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -4,12 +4,10 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions from comment.models import Comment -from comment.serializers import CommentRUDSerializer +from comment.serializers import CommentBaseSerializer from product import filters, serializers from product.models import Product, ProductType from utils.views import FavoritesCreateDestroyMixinView -from utils.pagination import PortionPagination -from django.conf import settings class ProductBaseView(generics.GenericAPIView): @@ -81,17 +79,17 @@ class ProductCommentListView(generics.ListAPIView): """View for return list of product comments.""" permission_classes = (permissions.AllowAny,) - serializer_class = serializers.ProductCommentCreateSerializer + serializer_class = serializers.ProductCommentBaseSerializer def get_queryset(self): """Override get_queryset method""" product = get_object_or_404(Product, slug=self.kwargs['slug']) - return product.comments.order_by('-created') + return product.comments.public(self.request.user).order_by('-created') class ProductCommentRUDView(generics.RetrieveUpdateDestroyAPIView): """View for retrieve/update/destroy product comment.""" - serializer_class = CommentRUDSerializer + serializer_class = serializers.ProductCommentBaseSerializer queryset = Product.objects.all() def get_object(self): diff --git a/apps/transfer/serializers/comments.py b/apps/transfer/serializers/comments.py index 73c90802..8322f89f 100644 --- a/apps/transfer/serializers/comments.py +++ b/apps/transfer/serializers/comments.py @@ -10,6 +10,7 @@ class CommentSerializer(serializers.Serializer): mark = serializers.DecimalField(max_digits=4, decimal_places=2, allow_null=True) account_id = serializers.IntegerField() establishment_id = serializers.CharField() + state = serializers.CharField() def validate(self, data): data.update({ @@ -18,14 +19,18 @@ class CommentSerializer(serializers.Serializer): 'mark': self.get_mark(data), 'content_object': self.get_content_object(data), 'user': self.get_account(data), + 'status': self.get_status(data), }) data.pop('establishment_id') data.pop('account_id') + data.pop('state') return data def create(self, validated_data): try: - return Comment.objects.create(**validated_data) + comment, _ = Comment.objects.get_or_create(old_id=validated_data.get('old_id'), + defaults=validated_data) + return comment except Exception as e: raise ValueError(f"Error creating comment with {validated_data}: {e}") @@ -48,3 +53,12 @@ class CommentSerializer(serializers.Serializer): if not data['mark']: return None return data['mark'] * -1 if data['mark'] < 0 else data['mark'] + + @staticmethod + def get_status(data): + if data.get('state'): + state = data.get('state') + if state == 'published': + return Comment.PUBLISHED + elif state == 'deleted': + return Comment.DELETED From fedf1400fd6e1cc42acecf9b5ae7f1ff502f3de7 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 15 Jan 2020 22:45:58 +0300 Subject: [PATCH 054/119] add M2M site feature relations to NavBarPermission model --- .../migrations/0048_auto_20200115_1944.py | 24 +++++++++++++++++++ apps/main/models.py | 3 +++ 2 files changed, 27 insertions(+) create mode 100644 apps/main/migrations/0048_auto_20200115_1944.py diff --git a/apps/main/migrations/0048_auto_20200115_1944.py b/apps/main/migrations/0048_auto_20200115_1944.py new file mode 100644 index 00000000..16e35d8f --- /dev/null +++ b/apps/main/migrations/0048_auto_20200115_1944.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.7 on 2020-01-15 19:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0047_auto_20200115_1013'), + ] + + operations = [ + migrations.AddField( + model_name='navigationbarpermission', + name='sections', + field=models.ManyToManyField(to='main.SiteFeature', verbose_name='sections'), + ), + migrations.AlterField( + model_name='navigationbarpermission', + name='section', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='old_sections', to='main.SiteFeature', verbose_name='section'), + ), + ] diff --git a/apps/main/models.py b/apps/main/models.py index a9e90895..7f54108a 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -549,8 +549,11 @@ class NavigationBarPermission(ProjectBaseMixin): ) section = models.ForeignKey('main.SiteFeature', + related_name='old_sections', on_delete=models.CASCADE, verbose_name=_('section')) + sections = models.ManyToManyField('main.SiteFeature', + verbose_name=_('sections')) permission_mode = models.PositiveSmallIntegerField(choices=PERMISSION_MODES, default=READ, help_text='READ - allows only retrieve data,' From 6e61587b27ed366b337f9cd45a02fbad91502eee Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 15 Jan 2020 23:19:17 +0300 Subject: [PATCH 055/119] refactor admin panel, remove unused field from model NavBarPermission --- apps/account/admin.py | 2 +- apps/comment/serializers/__init__.py | 2 +- apps/comment/serializers/back.py | 8 -------- apps/comment/views/back.py | 6 +++--- apps/main/admin.py | 5 +++-- ...49_remove_navigationbarpermission_section.py | 17 +++++++++++++++++ apps/main/models.py | 4 ---- apps/main/serializers/common.py | 4 ++-- 8 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 apps/main/migrations/0049_remove_navigationbarpermission_section.py diff --git a/apps/account/admin.py b/apps/account/admin.py index 04923cc9..b3302eae 100644 --- a/apps/account/admin.py +++ b/apps/account/admin.py @@ -7,7 +7,7 @@ from account import models @admin.register(models.Role) class RoleAdmin(admin.ModelAdmin): - list_display = ['role', 'country'] + list_display = ['id', 'role', 'country'] raw_id_fields = ['country', ] diff --git a/apps/comment/serializers/__init__.py b/apps/comment/serializers/__init__.py index 1a751cee..5c0d260f 100644 --- a/apps/comment/serializers/__init__.py +++ b/apps/comment/serializers/__init__.py @@ -1,4 +1,4 @@ +from .common import * from .mobile import * from .back import * from .web import * -from .common import * diff --git a/apps/comment/serializers/back.py b/apps/comment/serializers/back.py index 325086c0..8abdddf2 100644 --- a/apps/comment/serializers/back.py +++ b/apps/comment/serializers/back.py @@ -1,9 +1 @@ """Comment app common serializers.""" -from comment import models -from rest_framework import serializers - - -class CommentBaseSerializer(serializers.ModelSerializer): - class Meta: - model = models.Comment - fields = ('id', 'text', 'mark', 'user', 'object_id', 'content_type') \ No newline at end of file diff --git a/apps/comment/views/back.py b/apps/comment/views/back.py index a46b70cb..e5e1ee51 100644 --- a/apps/comment/views/back.py +++ b/apps/comment/views/back.py @@ -1,19 +1,19 @@ from rest_framework import generics, permissions -from comment.serializers import back as serializers +from comment.serializers import CommentBaseSerializer from comment import models from utils.permissions import IsCommentModerator, IsCountryAdmin class CommentLstView(generics.ListCreateAPIView): """Comment list create view.""" - serializer_class = serializers.CommentBaseSerializer + serializer_class = CommentBaseSerializer queryset = models.Comment.objects.all() # permission_classes = [permissions.IsAuthenticatedOrReadOnly| IsCommentModerator|IsCountryAdmin] class CommentRUDView(generics.RetrieveUpdateDestroyAPIView): """Comment RUD view.""" - serializer_class = serializers.CommentBaseSerializer + serializer_class = CommentBaseSerializer queryset = models.Comment.objects.all() permission_classes = [IsCommentModerator] # permission_classes = [IsCountryAdmin | IsCommentModerator] diff --git a/apps/main/admin.py b/apps/main/admin.py index 1cd6a774..b0fc63a3 100644 --- a/apps/main/admin.py +++ b/apps/main/admin.py @@ -25,6 +25,7 @@ class SiteFeatureAdmin(admin.ModelAdmin): @admin.register(models.Feature) class FeatureAdmin(admin.ModelAdmin): """Feature admin conf.""" + list_display = ['id', '__str__', 'priority', 'route', ] @admin.register(models.AwardType) @@ -54,6 +55,7 @@ class CarouselAdmin(admin.ModelAdmin): @admin.register(models.PageType) class PageTypeAdmin(admin.ModelAdmin): """PageType admin.""" + list_display = ['id', '__str__', ] @admin.register(models.Page) @@ -93,8 +95,7 @@ class PanelAdmin(admin.ModelAdmin): @admin.register(models.NavigationBarPermission) class NavigationBarPermissionAdmin(admin.ModelAdmin): """NavigationBarPermission admin.""" - list_display = ('section', 'permission_mode_display', ) - raw_id_fields = ('section', ) + list_display = ('id', 'permission_mode_display', ) def permission_mode_display(self, obj): """Get permission mode display.""" diff --git a/apps/main/migrations/0049_remove_navigationbarpermission_section.py b/apps/main/migrations/0049_remove_navigationbarpermission_section.py new file mode 100644 index 00000000..97358610 --- /dev/null +++ b/apps/main/migrations/0049_remove_navigationbarpermission_section.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.7 on 2020-01-15 20:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0048_auto_20200115_1944'), + ] + + operations = [ + migrations.RemoveField( + model_name='navigationbarpermission', + name='section', + ), + ] diff --git a/apps/main/models.py b/apps/main/models.py index 7f54108a..ea853c04 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -548,10 +548,6 @@ class NavigationBarPermission(ProjectBaseMixin): (WRITE, _('write')), ) - section = models.ForeignKey('main.SiteFeature', - related_name='old_sections', - on_delete=models.CASCADE, - verbose_name=_('section')) sections = models.ManyToManyField('main.SiteFeature', verbose_name=_('sections')) permission_mode = models.PositiveSmallIntegerField(choices=PERMISSION_MODES, diff --git a/apps/main/serializers/common.py b/apps/main/serializers/common.py index a954c649..6a556ee6 100644 --- a/apps/main/serializers/common.py +++ b/apps/main/serializers/common.py @@ -333,7 +333,7 @@ class PanelExecuteSerializer(serializers.ModelSerializer): class NavigationBarPermissionBaseSerializer(serializers.ModelSerializer): """Navigation bar permission serializer.""" - section = NavigationBarSectionBaseSerializer(read_only=True) + sections = NavigationBarSectionBaseSerializer(many=True, read_only=True) permission_mode_display = serializers.CharField(source='get_permission_mode_display', read_only=True) @@ -342,6 +342,6 @@ class NavigationBarPermissionBaseSerializer(serializers.ModelSerializer): model = models.NavigationBarPermission fields = [ 'id', - 'section', + 'sections', 'permission_mode_display', ] From e6dec3a1f05da929cd3eec4092a6f92f57de30c6 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Thu, 16 Jan 2020 06:51:39 +0000 Subject: [PATCH 056/119] fix optimize establishment preview images --- .../management/commands/establishment_optimize_preview_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/establishment/management/commands/establishment_optimize_preview_image.py b/apps/establishment/management/commands/establishment_optimize_preview_image.py index 50a73a26..f50c8801 100644 --- a/apps/establishment/management/commands/establishment_optimize_preview_image.py +++ b/apps/establishment/management/commands/establishment_optimize_preview_image.py @@ -24,5 +24,5 @@ class Command(BaseCommand): establishment.preview_image_url = get_thumbnail( file_=establishment.preview_image_url, **sorl_settings - ) + ).url establishment.save() From 78da617682c8e464a90c6cbffea4621d00bcda64 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Thu, 16 Jan 2020 07:13:31 +0000 Subject: [PATCH 057/119] fix optimize establishment preview images --- .../commands/establishment_optimize_preview_image.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/establishment/management/commands/establishment_optimize_preview_image.py b/apps/establishment/management/commands/establishment_optimize_preview_image.py index f50c8801..bfb5b047 100644 --- a/apps/establishment/management/commands/establishment_optimize_preview_image.py +++ b/apps/establishment/management/commands/establishment_optimize_preview_image.py @@ -13,16 +13,17 @@ class Command(BaseCommand): def handle(self, *args, **options): with transaction.atomic(): for establishment in Establishment.objects.all(): - if not image_url_valid(establishment.preview_image_url): + if isinstance(establishment.image_url, str) \ + or not image_url_valid(establishment.image_url): continue - _, width, height = get_image_meta_by_url(establishment.preview_image_url) + _, width, height = get_image_meta_by_url(establishment.image_url) sorl_settings = settings.SORL_THUMBNAIL_ALIASES[self.SORL_THUMBNAIL_ALIAS] sorl_width_height = sorl_settings['geometry_string'].split('x') if int(sorl_width_height[0]) > width or int(sorl_width_height[1]) > height: establishment.preview_image_url = get_thumbnail( - file_=establishment.preview_image_url, + file_=establishment.image_url, **sorl_settings ).url establishment.save() From f09b5c23670e923b57f217a07382dcf59e3cec3f Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Thu, 16 Jan 2020 07:29:36 +0000 Subject: [PATCH 058/119] fix condition for optimize establishment preview image --- .../commands/establishment_optimize_preview_image.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/establishment/management/commands/establishment_optimize_preview_image.py b/apps/establishment/management/commands/establishment_optimize_preview_image.py index bfb5b047..4cacea1a 100644 --- a/apps/establishment/management/commands/establishment_optimize_preview_image.py +++ b/apps/establishment/management/commands/establishment_optimize_preview_image.py @@ -13,17 +13,16 @@ class Command(BaseCommand): def handle(self, *args, **options): with transaction.atomic(): for establishment in Establishment.objects.all(): - if isinstance(establishment.image_url, str) \ - or not image_url_valid(establishment.image_url): + if establishment.preview_image_url is None \ + or not image_url_valid(establishment.preview_image_url): continue - - _, width, height = get_image_meta_by_url(establishment.image_url) + _, width, height = get_image_meta_by_url(establishment.preview_image_url) sorl_settings = settings.SORL_THUMBNAIL_ALIASES[self.SORL_THUMBNAIL_ALIAS] sorl_width_height = sorl_settings['geometry_string'].split('x') if int(sorl_width_height[0]) > width or int(sorl_width_height[1]) > height: establishment.preview_image_url = get_thumbnail( - file_=establishment.image_url, + file_=establishment.preview_image_url, **sorl_settings ).url establishment.save() From 9ea7c250853051aa63fc782b71f831fcf25b5862 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Thu, 16 Jan 2020 07:56:34 +0000 Subject: [PATCH 059/119] logs and save extension for preview --- .../commands/establishment_optimize_preview_image.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/establishment/management/commands/establishment_optimize_preview_image.py b/apps/establishment/management/commands/establishment_optimize_preview_image.py index 4cacea1a..cbb02b37 100644 --- a/apps/establishment/management/commands/establishment_optimize_preview_image.py +++ b/apps/establishment/management/commands/establishment_optimize_preview_image.py @@ -1,3 +1,5 @@ +import os + from django.conf import settings from django.core.management.base import BaseCommand from django.db import transaction @@ -21,8 +23,13 @@ class Command(BaseCommand): sorl_width_height = sorl_settings['geometry_string'].split('x') if int(sorl_width_height[0]) > width or int(sorl_width_height[1]) > height: + _, file_ext = os.path.splitext(establishment.preview_image_url) + + self.stdout.write(self.style.SUCCESS(f'Optimize: {establishment.preview_image_url}, extension: {file_ext}')) + establishment.preview_image_url = get_thumbnail( file_=establishment.preview_image_url, - **sorl_settings + **sorl_settings, + format='PNG' if file_ext[1:] == 'png' else 'JPEG' ).url establishment.save() From ca3d8a411c1ac717f14a2d0a898efa2b29093548 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Thu, 16 Jan 2020 08:11:29 +0000 Subject: [PATCH 060/119] format condition --- .../management/commands/establishment_optimize_preview_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/establishment/management/commands/establishment_optimize_preview_image.py b/apps/establishment/management/commands/establishment_optimize_preview_image.py index cbb02b37..7ab53a33 100644 --- a/apps/establishment/management/commands/establishment_optimize_preview_image.py +++ b/apps/establishment/management/commands/establishment_optimize_preview_image.py @@ -30,6 +30,6 @@ class Command(BaseCommand): establishment.preview_image_url = get_thumbnail( file_=establishment.preview_image_url, **sorl_settings, - format='PNG' if file_ext[1:] == 'png' else 'JPEG' + format='JPEG' if file_ext[1:] == 'jpg' else 'PNG' ).url establishment.save() From 0b261330e6b0cb6ee758f9c1de45a9bcf13715df Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 16 Jan 2020 11:19:32 +0300 Subject: [PATCH 061/119] some news optimizations --- apps/news/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/news/models.py b/apps/news/models.py index 79e2bbd0..3be956a7 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -78,7 +78,7 @@ class NewsQuerySet(TranslationQuerysetMixin): def with_base_related(self): """Return qs with related objects.""" - return self.select_related('news_type', 'country').prefetch_related('tags', 'tags__translation') + return self.select_related('news_type', 'country').prefetch_related('tags', 'tags__translation', 'gallery') def with_extended_related(self): """Return qs with related objects.""" @@ -306,8 +306,9 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin, @property def main_image(self): qs = self.news_gallery.main_image() - if qs.exists(): - return qs.order_by('-id').first().image + image_model = qs.order_by('-id').first() + if image_model is not None: + return image_model.image @property def image_url(self): From 7aab0bea31cd46a23adea90dd402086690e766d6 Mon Sep 17 00:00:00 2001 From: Ruslan Stepanov Date: Thu, 16 Jan 2020 08:24:29 +0000 Subject: [PATCH 062/119] Fixed misprint --- apps/tag/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/tag/views.py b/apps/tag/views.py index d13edd75..c621cd71 100644 --- a/apps/tag/views.py +++ b/apps/tag/views.py @@ -90,7 +90,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): params_type = query_params.get('product_type') week_days = tuple(map(_, ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"))) - short_week_days = tuple(map(_, ("Mon", "Tur", "Wed", "Thu", "Fri", "Sat", "Sun"))) + short_week_days = tuple(map(_, ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"))) flags = ('toque_number', 'wine_region', 'works_noon', 'works_evening', 'works_now', 'works_at_weekday') filter_flags = {flag_name: False for flag_name in flags} additional_flags = [] From 127d15b18184a860c895704d0c8e0423b20d0d63 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 16 Jan 2020 12:13:43 +0300 Subject: [PATCH 063/119] try to fix performance issues (cherry picked from commit 964ead1) --- apps/collection/views/common.py | 3 ++- apps/establishment/models.py | 5 +++-- project/settings/base.py | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/collection/views/common.py b/apps/collection/views/common.py index da9ded41..a6f1297f 100644 --- a/apps/collection/views/common.py +++ b/apps/collection/views/common.py @@ -65,7 +65,8 @@ class CollectionEstablishmentListView(CollectionListView): # May raise a permission denied self.check_object_permissions(self.request, collection) - return collection.establishments.published().annotate_in_favorites(self.request.user) + return collection.establishments.published().annotate_in_favorites(self.request.user) \ + .with_base_related().with_extended_related() # Guide diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 325d9d8d..53baac0b 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -775,8 +775,9 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, @property def main_image(self): qs = self.establishment_gallery.main_image() - if qs.exists(): - return qs.first().image + image_model = qs.first() + if image_model is not None: + return image_model.image @property def restaurant_category_indexing(self): diff --git a/project/settings/base.py b/project/settings/base.py index 0b5b9c20..755aa830 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -536,3 +536,5 @@ COOKIE_DOMAIN = None ELASTICSEARCH_DSL = {} ELASTICSEARCH_INDEX_NAMES = {} + +THUMBNAIL_FORCE_OVERWRITE = True From 017308da9566e6edfb50d2ef8f62a273479e20af Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 16 Jan 2020 14:19:56 +0300 Subject: [PATCH 064/119] refactored news transfer --- apps/news/transfer_data.py | 48 +++++++++++++++++++++----------------- apps/utils/methods.py | 18 ++++++++++++++ 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/apps/news/transfer_data.py b/apps/news/transfer_data.py index f4e16739..3586e357 100644 --- a/apps/news/transfer_data.py +++ b/apps/news/transfer_data.py @@ -11,6 +11,7 @@ from tag.models import TagCategory, Tag from translation.models import SiteInterfaceDictionary from transfer.models import PageTexts, PageCounters, PageMetadata from transfer.serializers.news import NewsSerializer +from utils.methods import transform_camelcase_to_underscore def add_locale(locale, data): @@ -36,35 +37,38 @@ def clear_old_news(): images.delete() news.delete() - # NewsType.objects.all().delete() print(f'Deleted {img_num} images') print(f'Deleted {news_num} news') def transfer_news(): - news_type, _ = NewsType.objects.get_or_create(name='news') + migrated_news_types = ('News', 'StaticPage', ) - queryset = PageTexts.objects.filter( - page__type='News', - ).annotate( - page__id=F('page__id'), - news_type_id=Value(news_type.id, output_field=IntegerField()), - page__created_at=F('page__created_at'), - page__account_id=F('page__account_id'), - page__state=F('page__state'), - page__template=F('page__template'), - page__site__country_code_2=F('page__site__country_code_2'), - page__root_title=F('page__root_title'), - page__attachment_suffix_url=F('page__attachment_suffix_url'), - page__published_at=F('page__published_at'), - ) + for news_type in migrated_news_types: + news_type_obj, _ = NewsType.objects.get_or_create( + name=transform_camelcase_to_underscore(news_type)) - serialized_data = NewsSerializer(data=list(queryset.values()), many=True) - if serialized_data.is_valid(): - serialized_data.save() - else: - pprint(f'News serializer errors: {serialized_data.errors}') + queryset = PageTexts.objects.filter( + page__type=news_type, + ).annotate( + page__id=F('page__id'), + news_type_id=Value(news_type_obj.id, output_field=IntegerField()), + page__created_at=F('page__created_at'), + page__account_id=F('page__account_id'), + page__state=F('page__state'), + page__template=F('page__template'), + page__site__country_code_2=F('page__site__country_code_2'), + page__root_title=F('page__root_title'), + page__attachment_suffix_url=F('page__attachment_suffix_url'), + page__published_at=F('page__published_at'), + ) + + serialized_data = NewsSerializer(data=list(queryset.values()), many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f'News serializer errors: {serialized_data.errors}') def update_en_gb_locales(): @@ -166,5 +170,5 @@ data_types = { update_en_gb_locales, add_views_count, add_tags, - ] + ], } diff --git a/apps/utils/methods.py b/apps/utils/methods.py index 741fd23b..b9855545 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -4,6 +4,7 @@ import random import re import string from collections import namedtuple +from functools import reduce from io import BytesIO import requests @@ -173,6 +174,23 @@ def transform_into_readable_str(raw_string: str, postfix: str = 'SectionNode'): return f"{''.join([i.capitalize() for i in result])}" +def transform_camelcase_to_underscore(raw_string: str): + """ + Transform str, i.e: + from + "ContentPage" + to + "content_page" + """ + + re_exp = r'[A-Z][^A-Z]*' + result = [i.lower() for i in re.findall(re_exp, raw_string) if i] + if result: + return reduce(lambda x, y: f'{x}_{y}', result) + else: + return raw_string + + def section_name_into_index_name(section_name: str): """ Transform slug into section name, i.e: From 14f19b2b4dbdf03ed50d42eb3273e8a65c82aefa Mon Sep 17 00:00:00 2001 From: "a.feteleu" Date: Thu, 16 Jan 2020 15:30:37 +0300 Subject: [PATCH 065/119] small fix --- apps/search_indexes/documents/news.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index 75174fae..6e287b0a 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -71,5 +71,5 @@ class NewsDocument(Document): The related_models option should be used with caution because it can lead in the index to the updating of a lot of items. """ - if isinstance(related_instance, models.NewsType): + if isinstance(related_instance, models.NewsType) and hasattr(related_instance, 'news_set'): return related_instance.news_set.all() From ad43e4a5f92df5ac1c1d919fea4f4987b5219227 Mon Sep 17 00:00:00 2001 From: dormantman Date: Thu, 16 Jan 2020 15:47:01 +0300 Subject: [PATCH 066/119] Added new route preview/slug// --- apps/news/serializers.py | 20 ++++++++++++++++++++ apps/news/urls/common.py | 1 + apps/news/views.py | 12 ++++++++++++ 3 files changed, 33 insertions(+) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index c0df73a4..d9050620 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -176,6 +176,26 @@ class NewsDetailWebSerializer(NewsDetailSerializer): return NewsSimilarListSerializer(obj.should_read(self.context['request'].user), many=True, read_only=True).data +class NewsPreviewWebSerializer(NewsDetailSerializer): + """News preview serializer for web users..""" + + same_theme = SerializerMethodField() + agenda = AgendaSerializer() + banner = NewsBannerSerializer() + + class Meta(NewsDetailSerializer.Meta): + """Meta class.""" + + fields = NewsDetailSerializer.Meta.fields + ( + 'same_theme', + 'agenda', + 'banner', + ) + + def get_same_theme(self, obj): + return NewsSimilarListSerializer(obj.same_theme(self.context['request'].user), many=True, read_only=True).data + + class NewsBackOfficeBaseSerializer(NewsBaseSerializer): """News back office base serializer.""" is_published = serializers.BooleanField(source='is_publish', read_only=True) diff --git a/apps/news/urls/common.py b/apps/news/urls/common.py index f5c809de..4668bbfc 100644 --- a/apps/news/urls/common.py +++ b/apps/news/urls/common.py @@ -7,4 +7,5 @@ common_urlpatterns = [ path('slug//', views.NewsDetailView.as_view(), name='rud'), path('slug//favorites/', views.NewsFavoritesCreateDestroyView.as_view(), name='create-destroy-favorites'), + path('preview/slug//', views.NewsPreviewView.as_view(), name='preview'), ] diff --git a/apps/news/views.py b/apps/news/views.py index 80f70b79..178c359f 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -70,6 +70,18 @@ class NewsDetailView(NewsMixinView, generics.RetrieveAPIView): return qs +class NewsPreviewView(NewsMixinView, generics.RetrieveAPIView): + """News preview view.""" + + lookup_field = None + serializer_class = serializers.NewsPreviewWebSerializer + + def get_queryset(self): + """Override get_queryset method.""" + qs = models.News.objects.all().annotate_in_favorites(self.request.user) + return qs + + class NewsTypeListView(generics.ListAPIView): """NewsType list view.""" From 2aec0bdfb426deaff9ab684122bedfbf54863696 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 16 Jan 2020 16:23:10 +0300 Subject: [PATCH 067/119] position filter for employee --- apps/account/models.py | 1 - apps/establishment/filters.py | 14 +++++++++--- apps/establishment/models.py | 42 +++++++++++++++++++++-------------- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/apps/account/models.py b/apps/account/models.py index c4d65d77..bca16dc7 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -1,6 +1,5 @@ """Account models""" from datetime import datetime -from tabnanny import verbose from django.conf import settings from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index 58826e19..be1c4785 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -1,8 +1,8 @@ """Establishment app filters.""" from django.core.validators import EMPTY_VALUES +from django.utils.translation import ugettext_lazy as _ from django_filters import rest_framework as filters from rest_framework.serializers import ValidationError -from django.utils.translation import ugettext_lazy as _ from establishment import models @@ -10,8 +10,8 @@ from establishment import models class EstablishmentFilter(filters.FilterSet): """Establishment filter set.""" - tag_id = filters.NumberFilter(field_name='tags__metadata__id',) - award_id = filters.NumberFilter(field_name='awards__id',) + tag_id = filters.NumberFilter(field_name='tags__metadata__id', ) + award_id = filters.NumberFilter(field_name='awards__id', ) search = filters.CharFilter(method='search_text') type = filters.CharFilter(method='by_type') subtype = filters.CharFilter(method='by_subtype') @@ -67,6 +67,7 @@ class EmployeeBackFilter(filters.FilterSet): """Employee filter set.""" search = filters.CharFilter(method='search_by_name_or_last_name') + position_id = filters.CharFilter(method='search_by_actual_position_id') class Meta: """Meta class.""" @@ -74,6 +75,7 @@ class EmployeeBackFilter(filters.FilterSet): model = models.Employee fields = ( 'search', + 'position_id', ) def search_by_name_or_last_name(self, queryset, name, value): @@ -82,6 +84,12 @@ class EmployeeBackFilter(filters.FilterSet): return queryset.search_by_name_or_last_name(value) return queryset + def search_by_actual_position_id(self, queryset, name, value): + """Search by actual position_id.""" + if value not in EMPTY_VALUES: + return queryset.search_by_position_id(value) + return queryset + class EmployeeBackSearchFilter(EmployeeBackFilter): def search_by_name_or_last_name(self, queryset, name, value): diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 53baac0b..698feeed 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -266,16 +266,16 @@ class EstablishmentQuerySet(models.QuerySet): 2 With ordering by distance. """ qs = self.similar_base(establishment) \ - .filter(**filters) + .filter(**filters) if establishment.address and establishment.address.coordinates: return Subquery( qs.annotate_distance(point=establishment.location) - .order_by('distance') - .distinct() - .values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS] + .order_by('distance') + .distinct() + .values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS] ) return Subquery( - qs.values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS] + qs.values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS] ) def similar_restaurants(self, restaurant): @@ -293,10 +293,10 @@ class EstablishmentQuerySet(models.QuerySet): } ) return self.filter(id__in=ids_by_subquery.queryset) \ - .annotate_intermediate_public_mark() \ - .annotate_mark_similarity(mark=restaurant.public_mark) \ - .order_by('mark_similarity') \ - .distinct('mark_similarity', 'id') + .annotate_intermediate_public_mark() \ + .annotate_mark_similarity(mark=restaurant.public_mark) \ + .order_by('mark_similarity') \ + .distinct('mark_similarity', 'id') def annotate_same_subtype(self, establishment): """Annotate flag same subtype.""" @@ -325,8 +325,8 @@ class EstablishmentQuerySet(models.QuerySet): similarity_rules['distinctions'].append('distance') return base_qs.has_published_reviews() \ - .order_by(*similarity_rules['ordering']) \ - .distinct(*similarity_rules['distinctions'], 'id') + .order_by(*similarity_rules['ordering']) \ + .distinct(*similarity_rules['distinctions'], 'id') def by_wine_region(self, wine_region): """ @@ -360,7 +360,7 @@ class EstablishmentQuerySet(models.QuerySet): similarity_rules['distinctions'].append('distance') return base_qs.order_by(*similarity_rules['ordering']) \ - .distinct(*similarity_rules['distinctions'], 'id') + .distinct(*similarity_rules['distinctions'], 'id') def similar_distilleries(self, distillery): """ @@ -380,9 +380,9 @@ class EstablishmentQuerySet(models.QuerySet): similarity_rules['distinctions'].append('distance') return base_qs.published() \ - .has_published_reviews() \ - .order_by(*similarity_rules['ordering']) \ - .distinct(*similarity_rules['distinctions'], 'id') + .has_published_reviews() \ + .order_by(*similarity_rules['ordering']) \ + .distinct(*similarity_rules['distinctions'], 'id') def similar_food_producers(self, food_producer): """ @@ -402,7 +402,7 @@ class EstablishmentQuerySet(models.QuerySet): similarity_rules['distinctions'].append('distance') return base_qs.order_by(*similarity_rules['ordering']) \ - .distinct(*similarity_rules['distinctions'], 'id') + .distinct(*similarity_rules['distinctions'], 'id') def last_reviewed(self, point: Point): """ @@ -653,7 +653,6 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, country = self.address.city.country return country.low_price, country.high_price - def set_establishment_type(self, establishment_type): self.establishment_type = establishment_type self.establishment_subtypes.exclude( @@ -991,6 +990,15 @@ class EmployeeQuerySet(models.QuerySet): """Search by name or last_name.""" return self._generic_search(value, ['name', 'last_name']) + def search_by_position_id(self, value): + """Search by position_id.""" + return self.filter( + Q(establishmentemployee__position_id=value), + Q(establishmentemployee__from_date__lte=datetime.now()), + Q(establishmentemployee__to_date__gte=datetime.now()) | + Q(establishmentemployee__to_date__isnull=True) + ) + def actual_establishment(self): e = EstablishmentEmployee.objects.actual().filter(employee=self) return self.prefetch_related(models.Prefetch('establishmentemployee_set', From 783e2e59311589af1ebd2c47d32539c7aa2ead11 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 16 Jan 2020 16:40:17 +0300 Subject: [PATCH 068/119] cascade create agenda --- apps/news/serializers.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 3e337de0..bed24907 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -22,7 +22,9 @@ from utils.serializers import ( class AgendaSerializer(ProjectModelSerializer): start_datetime = serializers.DateTimeField() end_datetime = serializers.DateTimeField() - address = AddressBaseSerializer() + address = AddressBaseSerializer(read_only=True) + address_id = serializers.PrimaryKeyRelatedField(write_only=True, queryset=location_models.Address.objects.all(), + source='address') event_name_translated = TranslatedField() content_translated = TranslatedField() @@ -36,7 +38,8 @@ class AgendaSerializer(ProjectModelSerializer): 'end_datetime', 'address', 'content_translated', - 'event_name_translated' + 'event_name_translated', + 'address_id', ) @@ -248,11 +251,17 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): user = request.user validated_data['created_by'] = user - agenda = validated_data.get('agenda') - if agenda is not None and (agenda.event_name is None or agenda.content is None): - raise serializers.ValidationError({'agenda': _('Agenda is empty')}) + agenda_data = validated_data.pop('agenda') + agenda_data['address_id'] = agenda_data.pop('address').pk + agenda_serializer = AgendaSerializer(data=agenda_data) + agenda_serializer.is_valid(raise_exception=True) + agenda = agenda_serializer.save() - return super().create(validated_data) + instance = super().create(validated_data) + instance.agenda = agenda + instance.save() + + return instance def update(self, instance, validated_data): slugs = validated_data.get('slugs') From 3d09333d31eb3a6e10f9b4927390a3a7cfd8518f Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 16 Jan 2020 16:51:33 +0300 Subject: [PATCH 069/119] toques for employees in BO --- apps/establishment/serializers/back.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 29fdd671..07117d70 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -229,6 +229,7 @@ class EmployeeBackSerializers(serializers.ModelSerializer): positions = serializers.SerializerMethodField() establishment = serializers.SerializerMethodField() awards = AwardSerializer(many=True, read_only=True) + toque_number = serializers.SerializerMethodField() def get_public_mark(self, obj): """Get last list actual public_mark""" @@ -236,6 +237,11 @@ class EmployeeBackSerializers(serializers.ModelSerializer): .values('establishment__public_mark').first() return qs['establishment__public_mark'] if qs else None + def get_toque_number(self, obj): + qs = obj.establishmentemployee_set.actual().order_by('-from_date') \ + .values('establishment__toque_number').first() + return qs['establishment__toque_number'] if qs else None + def get_positions(self, obj): """Get last list actual positions""" est_id = obj.establishmentemployee_set.actual(). \ From 881e4b11e23392ca70407cfdc650569e4d13bf06 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 16 Jan 2020 17:59:25 +0300 Subject: [PATCH 070/119] last ip for comments backoffice --- apps/comment/serializers/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/comment/serializers/common.py b/apps/comment/serializers/common.py index 1060f47f..c92e4266 100644 --- a/apps/comment/serializers/common.py +++ b/apps/comment/serializers/common.py @@ -13,6 +13,7 @@ class CommentBaseSerializer(serializers.ModelSerializer): source='user.cropped_image_url') status_display = serializers.CharField(read_only=True, source='get_status_display') + last_ip = serializers.IPAddressField(read_only=True, source='user.last_ip') class Meta: """Serializer for model Comment""" @@ -28,6 +29,7 @@ class CommentBaseSerializer(serializers.ModelSerializer): 'profile_pic', 'status', 'status_display', + 'last_ip', ] extra_kwargs = { 'status': {'read_only': True}, From 391df439c67f500c5923e98b6af195e2da6182f8 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 16 Jan 2020 18:05:02 +0300 Subject: [PATCH 071/119] fix reset password token generator --- apps/account/models.py | 3 +-- apps/account/views/web.py | 13 +++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/account/models.py b/apps/account/models.py index bca16dc7..ec01b68e 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -3,7 +3,6 @@ from datetime import datetime from django.conf import settings from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager -from django.contrib.auth.tokens import default_token_generator as password_token_generator from django.core.mail import send_mail from django.db import models from django.template.loader import render_to_string, get_template @@ -243,7 +242,7 @@ class User(AbstractUser): @property def reset_password_token(self): """Make a token for finish signup.""" - return password_token_generator.make_token(self) + return GMTokenGenerator(purpose=GMTokenGenerator.RESET_PASSWORD).make_token(self) @property def get_user_uidb64(self): diff --git a/apps/account/views/web.py b/apps/account/views/web.py index 0fc762f5..f8853d3e 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -1,6 +1,5 @@ """Web account views""" from django.conf import settings -from django.contrib.auth.tokens import default_token_generator as password_token_generator from django.shortcuts import get_object_or_404 from django.utils.encoding import force_text from django.utils.http import urlsafe_base64_decode @@ -10,6 +9,7 @@ from rest_framework.response import Response from account import tasks, models from account.serializers import web as serializers from utils import exceptions as utils_exceptions +from utils.models import GMTokenGenerator from utils.views import JWTGenericViewMixin @@ -40,22 +40,23 @@ class PasswordResetConfirmView(JWTGenericViewMixin, generics.GenericAPIView): queryset = models.User.objects.active() def get_object(self): - """Override get_object method""" + """Overridden get_object method""" queryset = self.filter_queryset(self.get_queryset()) uidb64 = self.kwargs.get('uidb64') user_id = force_text(urlsafe_base64_decode(uidb64)) token = self.kwargs.get('token') - obj = get_object_or_404(queryset, id=user_id) + user = get_object_or_404(queryset, id=user_id) - if not password_token_generator.check_token(user=obj, token=token): + if not GMTokenGenerator(GMTokenGenerator.RESET_PASSWORD).check_token( + user, token): raise utils_exceptions.NotValidTokenError() # May raise a permission denied - self.check_object_permissions(self.request, obj) + self.check_object_permissions(self.request, user) - return obj + return user def patch(self, request, *args, **kwargs): """Implement PATCH method""" From ea485d9960d3081385ccfaf3badffbd9a3da82fa Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 16 Jan 2020 19:21:47 +0300 Subject: [PATCH 072/119] optimize images at least for establishments --- apps/establishment/models.py | 12 ++++++-- apps/establishment/serializers/common.py | 38 ++++++++++++++++++++++-- apps/utils/models.py | 2 +- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 698feeed..6e37370c 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -122,7 +122,7 @@ class EstablishmentQuerySet(models.QuerySet): def with_base_related(self): """Return qs with related objects.""" return self.select_related('address', 'establishment_type'). \ - prefetch_related('tags', 'tags__translation') + prefetch_related('tags', 'tags__translation').with_main_image() def with_schedule(self): """Return qs with related schedule.""" @@ -506,6 +506,13 @@ class EstablishmentQuerySet(models.QuerySet): to_attr=attr_name) ) + def with_main_image(self): + return self.prefetch_related( + models.Prefetch('establishment_gallery', + queryset=EstablishmentGallery.objects.main_image(), + to_attr='main_image') + ) + class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin): @@ -772,7 +779,8 @@ class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin, return self.products.wines() @property - def main_image(self): + def _main_image(self): + """Please consider using prefetched query_set instead due to API performance issues""" qs = self.establishment_gallery.main_image() image_model = qs.first() if image_model is not None: diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 9ce576e5..473a2b35 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -1,4 +1,7 @@ """Establishment serializers.""" +import logging + +from django.conf import settings from django.utils.translation import ugettext_lazy as _ from phonenumber_field.phonenumber import to_python as str_to_phonenumber from rest_framework import serializers @@ -19,6 +22,7 @@ from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer from utils.serializers import (ProjectModelSerializer, TranslatedField, FavoritesCreateSerializer) +logger = logging.getLogger(__name__) class ContactPhonesSerializer(serializers.ModelSerializer): """Contact phone serializer""" @@ -319,16 +323,46 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): currency = CurrencySerializer() type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes') - image = serializers.URLField(source='image_url', read_only=True) + image = serializers.SerializerMethodField(read_only=True) wine_regions = EstablishmentWineRegionBaseSerializer(many=True, source='wine_origins_unique', read_only=True, allow_null=True) preview_image = serializers.URLField(source='preview_image_url', allow_null=True, read_only=True) tz = serializers.CharField(read_only=True, source='timezone_as_str') - new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True) + new_image = serializers.SerializerMethodField(allow_null=True, read_only=True) distillery_type = TagBaseSerializer(read_only=True, many=True, allow_null=True) + def get_image(self, obj): + if obj.main_image: + return obj.main_image[0].image.image.url if len(obj.main_image) else None + logging.info('Possibly not optimal image reading') + return obj._main_image # backwards compatibility + + def get_new_image(self, obj): + if hasattr(self, 'main_image') and hasattr(self, '_meta'): + if obj.main_image and len(obj.main_image): + main_image = obj.main_image[0].image + else: + logging.info('Possibly not optimal image reading') + main_image = obj._main_image + if main_image: + image = main_image + image_property = { + 'id': image.id, + 'title': image.title, + 'original_url': image.image.url, + 'orientation_display': image.get_orientation_display(), + 'auto_crop_images': {}, + } + crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES + if p.startswith(self._meta.model_name.lower())] + for crop in crop_parameters: + image_property['auto_crop_images'].update( + {crop: image.get_image_url(crop)} + ) + return image_property + class Meta: """Meta class.""" diff --git a/apps/utils/models.py b/apps/utils/models.py index e3faed4e..b7a2be49 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -221,7 +221,7 @@ class SORLImageMixin(models.Model): """Get image thumbnail url.""" crop_image = self.get_image(thumbnail_key) if hasattr(crop_image, 'url'): - return self.get_image(thumbnail_key).url + return crop_image.url def image_tag(self): """Admin preview tag.""" From 6c624115e6ee7edd9e21cfffa573c61d1f65e7c1 Mon Sep 17 00:00:00 2001 From: dormantman Date: Thu, 16 Jan 2020 20:36:19 +0300 Subject: [PATCH 073/119] Added agenda validate to update method --- apps/news/serializers.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index bed24907..791879ca 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -265,7 +265,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): def update(self, instance, validated_data): slugs = validated_data.get('slugs') - slugs_list = list(map(lambda x: x.lower(), slugs.values())) + slugs_list = list(map(lambda x: x.lower(), slugs.values() if slugs else ())) slugs_set = set(slugs_list) if slugs: if models.News.objects.filter( @@ -273,9 +273,27 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): ).exists() or len(slugs_list) != len(slugs_set): raise serializers.ValidationError({'slugs': _('Slug should be unique')}) - agenda = validated_data.get('agenda') - if agenda is not None and (agenda.event_name is None or agenda.content is None): - raise serializers.ValidationError({'agenda': _('Agenda is empty')}) + agenda_data = validated_data.get('agenda') + agenda = instance.agenda + + if agenda is None: + agenda_data['address_id'] = agenda_data.pop('address').pk + agenda_serializer = AgendaSerializer(data=agenda_data) + agenda_serializer.is_valid(raise_exception=True) + agenda_serializer.save() + + else: + agenda.start_datetime = agenda_data.pop( + 'start_datetime') if 'start_datetime' in agenda_data else agenda.start_datetime + agenda.end_datetime = agenda_data.pop( + 'end_datetime') if 'end_datetime' in agenda_data else agenda.end_datetime + agenda.address = agenda_data.pop( + 'address') if 'address' in agenda_data else agenda.address + agenda.event_name = agenda_data.pop( + 'event_name') if 'event_time' in agenda_data else agenda.event_name + agenda.content = agenda_data.pop( + 'content') if 'content' in agenda_data else agenda.content + agenda.save() return super().update(instance, validated_data) From a0fdef69b6f9603bf06af26dc85c016c3605ce62 Mon Sep 17 00:00:00 2001 From: dormantman Date: Thu, 16 Jan 2020 21:16:08 +0300 Subject: [PATCH 074/119] Fixed conditions for news methods --- apps/news/serializers.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 791879ca..e7d2e6d3 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -251,11 +251,14 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): user = request.user validated_data['created_by'] = user - agenda_data = validated_data.pop('agenda') - agenda_data['address_id'] = agenda_data.pop('address').pk - agenda_serializer = AgendaSerializer(data=agenda_data) - agenda_serializer.is_valid(raise_exception=True) - agenda = agenda_serializer.save() + agenda_data = validated_data.get('agenda') + agenda = None + + if agenda_data is not None: + agenda_data['address_id'] = agenda_data.pop('address').pk + agenda_serializer = AgendaSerializer(data=agenda_data) + agenda_serializer.is_valid(raise_exception=True) + agenda = agenda_serializer.save() instance = super().create(validated_data) instance.agenda = agenda @@ -276,13 +279,13 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer): agenda_data = validated_data.get('agenda') agenda = instance.agenda - if agenda is None: + if agenda is None and agenda_data is not None: agenda_data['address_id'] = agenda_data.pop('address').pk agenda_serializer = AgendaSerializer(data=agenda_data) agenda_serializer.is_valid(raise_exception=True) agenda_serializer.save() - else: + elif agenda_data is not None: agenda.start_datetime = agenda_data.pop( 'start_datetime') if 'start_datetime' in agenda_data else agenda.start_datetime agenda.end_datetime = agenda_data.pop( From 988de09efe23e7551584493cede9cf6a642ac2eb Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 17 Jan 2020 09:12:18 +0300 Subject: [PATCH 075/119] toque_number filter for employee --- apps/establishment/filters.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index be1c4785..32d29941 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -68,6 +68,7 @@ class EmployeeBackFilter(filters.FilterSet): search = filters.CharFilter(method='search_by_name_or_last_name') position_id = filters.CharFilter(method='search_by_actual_position_id') + toque_number = filters.NumberFilter(field_name='toque_number') class Meta: """Meta class.""" @@ -76,6 +77,7 @@ class EmployeeBackFilter(filters.FilterSet): fields = ( 'search', 'position_id', + 'toque_number', ) def search_by_name_or_last_name(self, queryset, name, value): From 38485292f9af3f645cc2097a8d5e460c0567936c Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 17 Jan 2020 09:21:51 +0300 Subject: [PATCH 076/119] public_mark filter for actual employee --- apps/establishment/filters.py | 10 +++++++++- apps/establishment/models.py | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index 32d29941..11f150b0 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -67,7 +67,8 @@ class EmployeeBackFilter(filters.FilterSet): """Employee filter set.""" search = filters.CharFilter(method='search_by_name_or_last_name') - position_id = filters.CharFilter(method='search_by_actual_position_id') + position_id = filters.NumberFilter(method='search_by_actual_position_id') + public_mark = filters.NumberFilter(method='search_by_public_mark') toque_number = filters.NumberFilter(field_name='toque_number') class Meta: @@ -77,6 +78,7 @@ class EmployeeBackFilter(filters.FilterSet): fields = ( 'search', 'position_id', + 'public_mark', 'toque_number', ) @@ -92,6 +94,12 @@ class EmployeeBackFilter(filters.FilterSet): return queryset.search_by_position_id(value) return queryset + def search_by_public_mark(self, queryset, name, value): + """Search by establishment public_mark.""" + if value not in EMPTY_VALUES: + return queryset.search_by_public_mark(value) + return queryset + class EmployeeBackSearchFilter(EmployeeBackFilter): def search_by_name_or_last_name(self, queryset, name, value): diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 6e37370c..867ea960 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1007,6 +1007,15 @@ class EmployeeQuerySet(models.QuerySet): Q(establishmentemployee__to_date__isnull=True) ) + def search_by_public_mark(self, value): + """Search by establishment public_mark.""" + return self.filter( + Q(establishmentemployee__establishment__public_mark=value), + Q(establishmentemployee__from_date__lte=datetime.now()), + Q(establishmentemployee__to_date__gte=datetime.now()) | + Q(establishmentemployee__to_date__isnull=True) + ) + def actual_establishment(self): e = EstablishmentEmployee.objects.actual().filter(employee=self) return self.prefetch_related(models.Prefetch('establishmentemployee_set', From f15a93f760a6352dacab65b2e12ac3a72a348289 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 17 Jan 2020 09:37:15 +0300 Subject: [PATCH 077/119] username, first_name, last_name filter for employee --- apps/establishment/filters.py | 8 ++++++++ apps/establishment/models.py | 24 +++++++++++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index 11f150b0..71084d9c 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -70,6 +70,7 @@ class EmployeeBackFilter(filters.FilterSet): position_id = filters.NumberFilter(method='search_by_actual_position_id') public_mark = filters.NumberFilter(method='search_by_public_mark') toque_number = filters.NumberFilter(field_name='toque_number') + username = filters.CharFilter(method='search_by_username_or_name') class Meta: """Meta class.""" @@ -80,6 +81,7 @@ class EmployeeBackFilter(filters.FilterSet): 'position_id', 'public_mark', 'toque_number', + 'username', ) def search_by_name_or_last_name(self, queryset, name, value): @@ -100,6 +102,12 @@ class EmployeeBackFilter(filters.FilterSet): return queryset.search_by_public_mark(value) return queryset + def search_by_username_or_name(self, queryset, name, value): + """Search by username or name.""" + if value not in EMPTY_VALUES: + return queryset.search_by_username_or_name(value) + return queryset + class EmployeeBackSearchFilter(EmployeeBackFilter): def search_by_name_or_last_name(self, queryset, name, value): diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 867ea960..80ac27c2 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -998,22 +998,32 @@ class EmployeeQuerySet(models.QuerySet): """Search by name or last_name.""" return self._generic_search(value, ['name', 'last_name']) - def search_by_position_id(self, value): - """Search by position_id.""" + def search_by_actual_employee(self): + """Search by actual employee.""" return self.filter( - Q(establishmentemployee__position_id=value), Q(establishmentemployee__from_date__lte=datetime.now()), Q(establishmentemployee__to_date__gte=datetime.now()) | Q(establishmentemployee__to_date__isnull=True) ) + def search_by_position_id(self, value): + """Search by position_id.""" + return self.search_by_actual_employee().filter( + Q(establishmentemployee__position_id=value), + ) + def search_by_public_mark(self, value): """Search by establishment public_mark.""" - return self.filter( + return self.search_by_actual_employee().filter( Q(establishmentemployee__establishment__public_mark=value), - Q(establishmentemployee__from_date__lte=datetime.now()), - Q(establishmentemployee__to_date__gte=datetime.now()) | - Q(establishmentemployee__to_date__isnull=True) + ) + + def search_by_username_or_name(self, value): + """Search by username or name.""" + return self.search_by_actual_employee().filter( + Q(user__username__icontains=value) | + Q(user__first_name__icontains=value) | + Q(user__last_name__icontains=value) ) def actual_establishment(self): From 85e5537b828f0b3f99ac683f91f07d98dfa9c7a6 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 17 Jan 2020 09:43:13 +0300 Subject: [PATCH 078/119] prefetch_related for employee serializers --- apps/establishment/views/back.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index bc22f5ce..c55c1a9c 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -34,7 +34,10 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView): lookup_field = 'slug' - queryset = models.Establishment.objects.all() + queryset = models.Establishment.objects.all().prefetch_related( + 'establishmentemployee_set', + 'establishmentemployee_set__establishment', + ) serializer_class = serializers.EstablishmentRUDSerializer permission_classes = [IsWineryReviewer | IsCountryAdmin | IsEstablishmentManager] @@ -171,7 +174,10 @@ class EmployeeListCreateView(generics.ListCreateAPIView): permission_classes = (permissions.AllowAny,) filter_class = filters.EmployeeBackFilter serializer_class = serializers.EmployeeBackSerializers - queryset = models.Employee.objects.all() + queryset = models.Employee.objects.all().prefetch_related( + 'establishmentemployee_set', + 'establishmentemployee_set__establishment', + ) class EmployeesListSearchViews(generics.ListAPIView): @@ -394,7 +400,7 @@ class EstablishmentPositionListView(generics.ListAPIView): class EstablishmentAdminView(generics.ListAPIView): """Establishment admin list view.""" serializer_class = serializers.EstablishmentAdminListSerializer - permission_classes = (permissions.IsAuthenticatedOrReadOnly, ) + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) def get_queryset(self): establishment = get_object_or_404( From 4880b2629d60bc6df66a128834e5b9389b94f9fb Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 17 Jan 2020 09:54:05 +0300 Subject: [PATCH 079/119] select_related for establishment types serializers --- apps/establishment/views/back.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index c55c1a9c..c365caa5 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -208,27 +208,27 @@ class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentTypeListCreateView(generics.ListCreateAPIView): """Establishment type list/create view.""" serializer_class = serializers.EstablishmentTypeBaseSerializer - queryset = models.EstablishmentType.objects.all() + queryset = models.EstablishmentType.objects.all().select_related('default_image') pagination_class = None class EstablishmentTypeRUDView(generics.RetrieveUpdateDestroyAPIView): """Establishment type retrieve/update/destroy view.""" serializer_class = serializers.EstablishmentTypeBaseSerializer - queryset = models.EstablishmentType.objects.all() + queryset = models.EstablishmentType.objects.all().select_related('default_image') class EstablishmentSubtypeListCreateView(generics.ListCreateAPIView): """Establishment subtype list/create view.""" serializer_class = serializers.EstablishmentSubTypeBaseSerializer - queryset = models.EstablishmentSubType.objects.all() + queryset = models.EstablishmentSubType.objects.all().select_related('default_image') pagination_class = None class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView): """Establishment subtype retrieve/update/destroy view.""" serializer_class = serializers.EstablishmentSubTypeBaseSerializer - queryset = models.EstablishmentSubType.objects.all() + queryset = models.EstablishmentSubType.objects.all().select_related('default_image') class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews, From 754b6d5b4f33da74db00b31170c525a60718ba93 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 17 Jan 2020 10:02:40 +0300 Subject: [PATCH 080/119] fix collection guides --- apps/collection/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/collection/models.py b/apps/collection/models.py index e7f02591..0457f38d 100644 --- a/apps/collection/models.py +++ b/apps/collection/models.py @@ -165,7 +165,7 @@ class GuideQuerySet(models.QuerySet): def with_base_related(self): """Return QuerySet with related.""" - return self.select_related('guide_type', 'site') + return self.select_related('site', ) def by_country_id(self, country_id): """Return QuerySet filtered by country code.""" From 0695cdce6ba4cc1b3c7764b756168b9fdb229f9e Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 17 Jan 2020 11:36:13 +0300 Subject: [PATCH 081/119] available_for_events field for employee --- .../0074_employee_available_for_events.py | 18 ++++++++++++++++++ apps/establishment/models.py | 2 ++ apps/establishment/serializers/back.py | 3 ++- apps/establishment/views/back.py | 5 ++++- 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 apps/establishment/migrations/0074_employee_available_for_events.py diff --git a/apps/establishment/migrations/0074_employee_available_for_events.py b/apps/establishment/migrations/0074_employee_available_for_events.py new file mode 100644 index 00000000..cd5622e8 --- /dev/null +++ b/apps/establishment/migrations/0074_employee_available_for_events.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2020-01-17 08:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0073_auto_20200115_1710'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='available_for_events', + field=models.BooleanField(default=False, verbose_name='Available for events'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 80ac27c2..01615271 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1070,6 +1070,8 @@ class Employee(BaseAttributes): verbose_name=_('Tags')) # old_id = profile_id old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True) + available_for_events = models.BooleanField(_('Available for events'), default=False) + photo = models objects = EmployeeQuerySet.as_manager() diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 07117d70..e1818cfc 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -286,7 +286,8 @@ class EmployeeBackSerializers(serializers.ModelSerializer): 'birth_date', 'email', 'phone', - 'toque_number' + 'toque_number', + 'available_for_events', ] diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index c365caa5..b34cc845 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -184,7 +184,10 @@ class EmployeesListSearchViews(generics.ListAPIView): """Employee search view""" pagination_class = None permission_classes = (permissions.AllowAny,) - queryset = models.Employee.objects.all() + queryset = models.Employee.objects.all().prefetch_related( + 'establishmentemployee_set', + 'establishmentemployee_set__establishment', + ) filter_class = filters.EmployeeBackSearchFilter serializer_class = serializers.EmployeeBackSerializers From a915cff73d6121a8ea0e1eaf8ace1ec972828f31 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 17 Jan 2020 13:56:58 +0300 Subject: [PATCH 082/119] photo for employee --- .../migrations/0075_employee_photo.py | 20 +++++++++++++ apps/establishment/models.py | 29 ++++++++++++++++++- apps/establishment/serializers/back.py | 4 ++- apps/establishment/views/back.py | 2 +- 4 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 apps/establishment/migrations/0075_employee_photo.py diff --git a/apps/establishment/migrations/0075_employee_photo.py b/apps/establishment/migrations/0075_employee_photo.py new file mode 100644 index 00000000..3b505680 --- /dev/null +++ b/apps/establishment/migrations/0075_employee_photo.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.7 on 2020-01-17 10:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('gallery', '0008_merge_20191212_0752'), + ('establishment', '0074_employee_available_for_events'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='photo', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employee_photo', to='gallery.Image', verbose_name='image instance of model Image'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 01615271..d7cc9e7e 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1071,7 +1071,10 @@ class Employee(BaseAttributes): # old_id = profile_id old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True) available_for_events = models.BooleanField(_('Available for events'), default=False) - photo = models + photo = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL, + blank=True, null=True, default=None, + related_name='employee_photo', + verbose_name=_('image instance of model Image')) objects = EmployeeQuerySet.as_manager() @@ -1085,6 +1088,30 @@ class Employee(BaseAttributes): GinIndex(fields=('last_name',)) ] + @property + def image_object(self): + """Return image object.""" + return self.photo.image if self.photo else None + + @property + def crop_image(self): + if hasattr(self, 'photo') and hasattr(self, '_meta'): + if self.photo: + image_property = { + 'id': self.photo.id, + 'title': self.photo.title, + 'original_url': self.photo.image.url, + 'orientation_display': self.photo.get_orientation_display(), + 'auto_crop_images': {}, + } + crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES + if p.startswith(self._meta.model_name.lower())] + for crop in crop_parameters: + image_property['auto_crop_images'].update( + {crop: self.photo.get_image_url(crop)} + ) + return image_property + class EstablishmentScheduleQuerySet(models.QuerySet): """QuerySet for model EstablishmentSchedule""" diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index e1818cfc..a6246d47 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -11,7 +11,7 @@ from location.serializers import AddressDetailSerializer, TranslatedField from main.models import Currency from main.serializers import AwardSerializer from utils.decorators import with_base_attributes -from utils.serializers import TimeZoneChoiceField +from utils.serializers import TimeZoneChoiceField, ImageBaseSerializer def phones_handler(phones_list, establishment): @@ -230,6 +230,7 @@ class EmployeeBackSerializers(serializers.ModelSerializer): establishment = serializers.SerializerMethodField() awards = AwardSerializer(many=True, read_only=True) toque_number = serializers.SerializerMethodField() + photo = ImageBaseSerializer(source='crop_image', read_only=True) def get_public_mark(self, obj): """Get last list actual public_mark""" @@ -288,6 +289,7 @@ class EmployeeBackSerializers(serializers.ModelSerializer): 'phone', 'toque_number', 'available_for_events', + 'photo', ] diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index b34cc845..efa93316 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -187,7 +187,7 @@ class EmployeesListSearchViews(generics.ListAPIView): queryset = models.Employee.objects.all().prefetch_related( 'establishmentemployee_set', 'establishmentemployee_set__establishment', - ) + ).select_related('photo') filter_class = filters.EmployeeBackSearchFilter serializer_class = serializers.EmployeeBackSerializers From 4a7eb2229165afcda7f7ba09f2616795a80a8101 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 17 Jan 2020 14:02:46 +0300 Subject: [PATCH 083/119] order by position for establishment employee --- apps/establishment/views/back.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index efa93316..578f6b6a 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -196,10 +196,11 @@ class EstablishmentEmployeeListView(generics.ListCreateAPIView): """Establishment emplyoees list view.""" permission_classes = (permissions.AllowAny,) serializer_class = serializers.EstablishmentEmployeeBackSerializer + pagination_class = None def get_queryset(self): establishment_id = self.kwargs['establishment_id'] - return models.EstablishmentEmployee.objects.filter(establishment__id=establishment_id) + return models.EstablishmentEmployee.objects.filter(establishment__id=establishment_id).order_by('position') class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): From abfd4d728895e682d8b6f3c2637feeafa92612df Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 17 Jan 2020 14:33:44 +0300 Subject: [PATCH 084/119] full-text search employees --- apps/establishment/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index 71084d9c..14d6ae05 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -87,7 +87,7 @@ class EmployeeBackFilter(filters.FilterSet): def search_by_name_or_last_name(self, queryset, name, value): """Search by name or last name.""" if value not in EMPTY_VALUES: - return queryset.search_by_name_or_last_name(value) + return queryset.trigram_search(value) return queryset def search_by_actual_position_id(self, queryset, name, value): From a8ea238bfce9db6faaa917644d7501d848b633a6 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 17 Jan 2020 16:02:00 +0300 Subject: [PATCH 085/119] some optimize for employee list --- apps/establishment/serializers/back.py | 37 +++++++++++++++++--------- apps/establishment/views/back.py | 1 + 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index a6246d47..8f9631f1 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -1,3 +1,6 @@ +from functools import lru_cache + +from django.db.models import F from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -232,21 +235,32 @@ class EmployeeBackSerializers(serializers.ModelSerializer): toque_number = serializers.SerializerMethodField() photo = ImageBaseSerializer(source='crop_image', read_only=True) + @staticmethod + @lru_cache(maxsize=32) + def get_qs(obj): + return obj.establishmentemployee_set.actual().annotate( + public_mark=F('establishment__public_mark'), + est_id=F('establishment__id'), + est_slug=F('establishment__slug'), + toque_number=F('establishment__toque_number'), + ).order_by('-from_date').first() + def get_public_mark(self, obj): """Get last list actual public_mark""" - qs = obj.establishmentemployee_set.actual().order_by('-from_date') \ - .values('establishment__public_mark').first() - return qs['establishment__public_mark'] if qs else None + qs = self.get_qs(obj) + if qs: + return qs.public_mark + return None def get_toque_number(self, obj): - qs = obj.establishmentemployee_set.actual().order_by('-from_date') \ - .values('establishment__toque_number').first() - return qs['establishment__toque_number'] if qs else None + qs = self.get_qs(obj) + if qs: + return qs.toque_number + return None def get_positions(self, obj): """Get last list actual positions""" - est_id = obj.establishmentemployee_set.actual(). \ - order_by('-from_date').first() + est_id = self.get_qs(obj) if not est_id: return None @@ -261,15 +275,14 @@ class EmployeeBackSerializers(serializers.ModelSerializer): def get_establishment(self, obj): """Get last actual establishment""" - est = obj.establishmentemployee_set.actual().order_by('-from_date') \ - .first() + est = self.get_qs(obj) if not est: return None return { - "id": est.establishment.id, - "slug": est.establishment.slug + "id": est.est_id, + "slug": est.est_slug } class Meta: diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 578f6b6a..f98747b6 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -177,6 +177,7 @@ class EmployeeListCreateView(generics.ListCreateAPIView): queryset = models.Employee.objects.all().prefetch_related( 'establishmentemployee_set', 'establishmentemployee_set__establishment', + 'awards', ) From 8b4a6b0b740270f98eaf7e036aa442891c12d4a9 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 17 Jan 2020 16:13:27 +0300 Subject: [PATCH 086/119] fix employees search --- apps/establishment/models.py | 38 +++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index d7cc9e7e..5f8ec73d 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -11,12 +11,12 @@ 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 +from django.contrib.postgres.search import TrigramDistance, TrigramSimilarity from django.contrib.postgres.indexes import GinIndex from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models -from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q, Prefetch +from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q, Prefetch, Sum from django.utils import timezone from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField @@ -990,9 +990,37 @@ class EmployeeQuerySet(models.QuerySet): def trigram_search(self, search_value: str): """Search with mistakes by name or last name.""" return self.annotate( - name_distance=TrigramDistance('name', search_value.lower()), - last_name_distance=TrigramDistance('last_name', search_value.lower()), - ).filter(Q(name_distance__lte=0.7) | Q(last_name_distance__lte=0.7)).order_by('name_distance') + search_exact_match=models.Case( + models.When(Q(name__iexact=search_value) | Q(last_name__iexact=search_value), + then=100), + default=0, + output_field=models.FloatField() + ), + search_contains_match=models.Case( + models.When(Q(name__icontains=search_value) | Q(last_name__icontains=search_value), + then=50), + default=0, + output_field=models.FloatField() + ), + search_name_similarity=models.Case( + models.When( + Q(name__isnull=False), + then=TrigramSimilarity('name', search_value.lower()) + ), + default=0, + output_field=models.FloatField() + ), + search_last_name_similarity=models.Case( + models.When( + Q(last_name__isnull=False), + then=TrigramSimilarity('last_name', search_value.lower()) + ), + default=0, + output_field=models.FloatField() + ), + relevance=(F('search_name_similarity') + F('search_exact_match') + + F('search_contains_match') + F('search_last_name_similarity')) + ).filter(relevance__gte=0.3).order_by('-relevance') def search_by_name_or_last_name(self, value): """Search by name or last_name.""" From c3ed88339e264a8792cb8283977f7fb5df75100a Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 17 Jan 2020 17:07:23 +0300 Subject: [PATCH 087/119] order BO user by date they joined --- apps/account/views/back.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/account/views/back.py b/apps/account/views/back.py index 49185557..09ab1048 100644 --- a/apps/account/views/back.py +++ b/apps/account/views/back.py @@ -41,7 +41,8 @@ class UserListView(generics.ListCreateAPIView): 'is_active', 'is_superuser', 'roles', - 'last_login' + 'last_login', + 'date_joined', ) From 2b28a562d22167d556e0d01044faebf6db8b7677 Mon Sep 17 00:00:00 2001 From: Ruslan Stepanov Date: Fri, 17 Jan 2020 14:31:15 +0000 Subject: [PATCH 088/119] Added type for pop filter in tags --- apps/tag/views.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/tag/views.py b/apps/tag/views.py index c621cd71..e0b19fa5 100644 --- a/apps/tag/views.py +++ b/apps/tag/views.py @@ -129,10 +129,19 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): result_list.append(wine_regions) + item_types = { + 'pop': 'pop' + } + for item in result_list: if 'filters' in item: item['filters'].sort(key=lambda x: x.get('label_translated')) + index_name = item.get('index_name') + + if index_name in item_types: + item['type'] = item_types[index_name] + if filter_flags['toque_number']: toques = { "index_name": "toque_number", From 0a832136d3ffc7f2a2a2aff29097649b5bb4a9c3 Mon Sep 17 00:00:00 2001 From: dormantman Date: Thu, 16 Jan 2020 23:53:47 +0300 Subject: [PATCH 089/119] Added update establishment image urls command --- .../update_establishment_image_urls.py | 46 +++++++++++++++++++ apps/establishment/tasks.py | 37 +++++++++++++-- 2 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 apps/establishment/management/commands/update_establishment_image_urls.py diff --git a/apps/establishment/management/commands/update_establishment_image_urls.py b/apps/establishment/management/commands/update_establishment_image_urls.py new file mode 100644 index 00000000..6211c520 --- /dev/null +++ b/apps/establishment/management/commands/update_establishment_image_urls.py @@ -0,0 +1,46 @@ +import math + +from django.core.management.base import BaseCommand +from celery import group +from establishment.models import Establishment +from establishment.tasks import update_establishment_image_urls + + +class Command(BaseCommand): + help = """ + Updating image links for establishments. + Run command ./manage.py update_establishment_image_urls + """ + + def add_arguments(self, parser): + parser.add_argument( + '--bucket_size', + type=int, + help='Size of one basket to update' + ) + + def handle(self, *args, **kwargs): + bucket_size = kwargs.pop('bucket_size') if kwargs.get('bucket_size') else 128 + + objects = Establishment.objects.all() + objects_size = objects.count() + summary_tasks = math.ceil(objects_size / bucket_size) + + tasks = [] + + for index in range(0, objects_size, bucket_size): + bucket = objects[index: index + bucket_size] + + task = update_establishment_image_urls.s( + (index + bucket_size) / bucket_size, summary_tasks, + bucket.values_list('id', flat=True) + ) + + tasks.append(task) + + self.stdout.write(self.style.WARNING(f'Created all celery update tasks.\n')) + + job = group(*tasks) + job.delay() + + self.stdout.write(self.style.WARNING(f'Done all celery update tasks.\n')) diff --git a/apps/establishment/tasks.py b/apps/establishment/tasks.py index 4197b65d..2df4711d 100644 --- a/apps/establishment/tasks.py +++ b/apps/establishment/tasks.py @@ -1,17 +1,15 @@ """Establishment app tasks.""" import logging +import requests from celery import shared_task from celery.schedules import crontab from celery.task import periodic_task - -from django.core import management -from django_elasticsearch_dsl.management.commands import search_index - from django_elasticsearch_dsl.registries import registry + from establishment import models +from establishment.models import Establishment from location.models import Country -from search_indexes.documents.establishment import EstablishmentDocument logger = logging.getLogger(__name__) @@ -28,6 +26,7 @@ def recalculate_price_levels_by_country(country_id): establishment.recalculate_price_level(low_price=country.low_price, high_price=country.high_price) + # @periodic_task(run_every=crontab(minute=59)) # def rebuild_establishment_indices(): # management.call_command(search_index.Command(), action='populate', models=[models.Establishment.__name__], @@ -50,3 +49,31 @@ def recalculation_public_mark(establishment_id): establishment = models.Establishment.objects.get(id=establishment_id) establishment.recalculate_public_mark() establishment.recalculate_toque_number() + + +@shared_task +def update_establishment_image_urls(part_number: int, summary_tasks: int, bucket_ids: list): + queryset = Establishment.objects.filter(id__in=bucket_ids) + + for establishment in queryset: + for data in [ + ('image_url', establishment.image_url), + ('preview_image_url', establishment.preview_image_url) + ]: + attr, url = data + + if establishment.image_url is not None: + try: + response = requests.get(url, allow_redirects=True) + + if response.status_code != 200: + setattr(establishment, attr, None) + + except ( + requests.exceptions.ConnectionError, + requests.exceptions.ConnectTimeout + ): + setattr(establishment, attr, None) + + logger.info(f'The {part_number}th part of the image update ' + f'from {summary_tasks} parts was completed') From 294c468405e2c5a944f6f615eb4bad45f60a52c5 Mon Sep 17 00:00:00 2001 From: dormantman Date: Fri, 17 Jan 2020 14:23:53 +0300 Subject: [PATCH 090/119] Added live link part --- apps/establishment/tasks.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/establishment/tasks.py b/apps/establishment/tasks.py index 2df4711d..332a3d7a 100644 --- a/apps/establishment/tasks.py +++ b/apps/establishment/tasks.py @@ -56,10 +56,14 @@ def update_establishment_image_urls(part_number: int, summary_tasks: int, bucket queryset = Establishment.objects.filter(id__in=bucket_ids) for establishment in queryset: - for data in [ + live_link = None + + image_urls = [ ('image_url', establishment.image_url), ('preview_image_url', establishment.preview_image_url) - ]: + ] + + for data in image_urls: attr, url = data if establishment.image_url is not None: @@ -69,11 +73,21 @@ def update_establishment_image_urls(part_number: int, summary_tasks: int, bucket if response.status_code != 200: setattr(establishment, attr, None) + else: + live_link = url + except ( requests.exceptions.ConnectionError, requests.exceptions.ConnectTimeout ): setattr(establishment, attr, None) + if live_link is not None: + if establishment.image_url is None: + establishment.image_url = live_link + + elif establishment.preview_image_url is None: + establishment.preview_image_url = live_link + logger.info(f'The {part_number}th part of the image update ' f'from {summary_tasks} parts was completed') From 281aca4fe5f7d72600731c48edf2d0748d233d26 Mon Sep 17 00:00:00 2001 From: Ruslan Stepanov Date: Fri, 17 Jan 2020 13:26:29 +0000 Subject: [PATCH 091/119] Added establishment saving --- apps/establishment/tasks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/establishment/tasks.py b/apps/establishment/tasks.py index 332a3d7a..3dfe79c1 100644 --- a/apps/establishment/tasks.py +++ b/apps/establishment/tasks.py @@ -88,6 +88,8 @@ def update_establishment_image_urls(part_number: int, summary_tasks: int, bucket elif establishment.preview_image_url is None: establishment.preview_image_url = live_link - + + establishment.save() + logger.info(f'The {part_number}th part of the image update ' f'from {summary_tasks} parts was completed') From 9ef26594b61a1ad9beb6b640c8d6e97a13d6275f Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 17 Jan 2020 17:32:39 +0300 Subject: [PATCH 092/119] code improvements --- .../management/commands/update_establishment_image_urls.py | 4 ++-- apps/establishment/tasks.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/establishment/management/commands/update_establishment_image_urls.py b/apps/establishment/management/commands/update_establishment_image_urls.py index 6211c520..c6b796ae 100644 --- a/apps/establishment/management/commands/update_establishment_image_urls.py +++ b/apps/establishment/management/commands/update_establishment_image_urls.py @@ -20,7 +20,7 @@ class Command(BaseCommand): ) def handle(self, *args, **kwargs): - bucket_size = kwargs.pop('bucket_size') if kwargs.get('bucket_size') else 128 + bucket_size = kwargs.get('bucket_size', 128) objects = Establishment.objects.all() objects_size = objects.count() @@ -43,4 +43,4 @@ class Command(BaseCommand): job = group(*tasks) job.delay() - self.stdout.write(self.style.WARNING(f'Done all celery update tasks.\n')) + self.stdout.write(self.style.SUCCESS(f'Done all celery update tasks.\n')) diff --git a/apps/establishment/tasks.py b/apps/establishment/tasks.py index 3dfe79c1..d3816fa1 100644 --- a/apps/establishment/tasks.py +++ b/apps/establishment/tasks.py @@ -8,7 +8,6 @@ from celery.task import periodic_task from django_elasticsearch_dsl.registries import registry from establishment import models -from establishment.models import Establishment from location.models import Country logger = logging.getLogger(__name__) @@ -53,7 +52,7 @@ def recalculation_public_mark(establishment_id): @shared_task def update_establishment_image_urls(part_number: int, summary_tasks: int, bucket_ids: list): - queryset = Establishment.objects.filter(id__in=bucket_ids) + queryset = models.Establishment.objects.filter(id__in=bucket_ids) for establishment in queryset: live_link = None From 0bb521b479e9a504bcee31d3f7540ba2e5b69b94 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 17 Jan 2020 17:35:50 +0300 Subject: [PATCH 093/119] fix command --- .../management/commands/update_establishment_image_urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/establishment/management/commands/update_establishment_image_urls.py b/apps/establishment/management/commands/update_establishment_image_urls.py index c6b796ae..397d4f0d 100644 --- a/apps/establishment/management/commands/update_establishment_image_urls.py +++ b/apps/establishment/management/commands/update_establishment_image_urls.py @@ -16,6 +16,7 @@ class Command(BaseCommand): parser.add_argument( '--bucket_size', type=int, + default=128, help='Size of one basket to update' ) From a26109a8da8f9c5a1ea5032e7600a640bda33bc1 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 17 Jan 2020 17:38:50 +0300 Subject: [PATCH 094/119] added endpoint /api/back/account/role-tab/ --- apps/account/filters.py | 29 ++++++++++++++++++ apps/account/models.py | 51 ++++++++++++++++++++++++++++---- apps/account/serializers/back.py | 6 ++++ apps/account/urls/back.py | 1 + apps/account/views/back.py | 44 ++++++++++++++++++++------- apps/tag/filters.py | 1 + 6 files changed, 115 insertions(+), 17 deletions(-) create mode 100644 apps/account/filters.py diff --git a/apps/account/filters.py b/apps/account/filters.py new file mode 100644 index 00000000..996c09b8 --- /dev/null +++ b/apps/account/filters.py @@ -0,0 +1,29 @@ +"""Account app filters.""" +from django.core.validators import EMPTY_VALUES +from django_filters import rest_framework as filters + +from account import models + + +class AccountBackOfficeFilter(filters.FilterSet): + """Account filter set.""" + + role = filters.MultipleChoiceFilter(choices=models.Role.ROLE_CHOICES, + method='filter_by_roles') + + class Meta: + """Meta class.""" + + model = models.User + fields = ( + 'role', + 'email_confirmed', + 'is_staff', + 'is_active', + 'is_superuser', + ) + + def filter_by_roles(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_roles(value) + return queryset diff --git a/apps/account/models.py b/apps/account/models.py index ec01b68e..e39e77a4 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -22,6 +22,19 @@ from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin from utils.tokens import GMRefreshToken +class RoleQuerySet(models.QuerySet): + + def annotate_role_name(self): + return self.annotate(role_name=models.Case(*self.model.role_condition_expressions(), + output_field=models.CharField())) + + def annotate_role_counter(self): + return self.annotate( + role_counter=models.Count('userrole', + distinct=True, + filter=models.Q(userrole__state=UserRole.VALIDATED))) + + class Role(ProjectBaseMixin): """Base Role model.""" STANDARD_USER = 1 @@ -44,12 +57,12 @@ class Role(ProjectBaseMixin): (CONTENT_PAGE_MANAGER, _('Content page manager')), (ESTABLISHMENT_MANAGER, _('Establishment manager')), (REVIEWER_MANGER, _('Reviewer manager')), - (RESTAURANT_REVIEWER, 'Restaurant reviewer'), - (SALES_MAN, 'Sales man'), - (WINERY_REVIEWER, 'Winery reviewer'), - (SELLER, 'Seller'), - (LIQUOR_REVIEWER, 'Liquor reviewer'), - (PRODUCT_REVIEWER, 'Product reviewer'), + (RESTAURANT_REVIEWER, _('Restaurant reviewer')), + (SALES_MAN, _('Sales man')), + (WINERY_REVIEWER, _('Winery reviewer')), + (SELLER, _('Seller')), + (LIQUOR_REVIEWER, _('Liquor reviewer')), + (PRODUCT_REVIEWER, _('Product reviewer')), ) role = models.PositiveIntegerField(verbose_name=_('Role'), choices=ROLE_CHOICES, @@ -67,6 +80,22 @@ class Role(ProjectBaseMixin): help_text='navigation bar item permission', verbose_name=_('navigation bar permission')) + objects = RoleQuerySet.as_manager() + + @classmethod + def role_names(cls): + return [role_name._proxy____args[0] + for role_name in dict(cls.ROLE_CHOICES).values()] + + @classmethod + def role_condition_expressions(cls) -> list: + role_choices = {role_id: role_name._proxy____args[0] + for role_id, role_name in dict(cls.ROLE_CHOICES).items()} + + whens = [models.When(role=role_id, then=models.Value(role_name)) + for role_id, role_name in role_choices.items()] + return whens + class UserManager(BaseUserManager): """Extended manager for User model.""" @@ -347,6 +376,14 @@ class User(AbstractUser): return set(result) +class UserRoleQueryset(models.QuerySet): + """QuerySet for model UserRole.""" + + def country_admin_role(self): + return self.filter(role__role=self.model.role.field.target_field.model.COUNTRY_ADMIN, + state=self.model.VALIDATED) + + class UserRole(ProjectBaseMixin): """UserRole model.""" VALIDATED = 'validated' @@ -377,6 +414,8 @@ class UserRole(ProjectBaseMixin): help_text='A user (REQUESTER) who requests a ' 'role change for a USER') + objects = UserRoleQueryset.as_manager() + class Meta: unique_together = ['user', 'role', 'establishment', 'state'] diff --git a/apps/account/serializers/back.py b/apps/account/serializers/back.py index deac6f6c..05f4ece1 100644 --- a/apps/account/serializers/back.py +++ b/apps/account/serializers/back.py @@ -148,3 +148,9 @@ class UserRoleSerializer(serializers.ModelSerializer): 'user', 'establishment' ] + + +class RoleTabRetrieveSerializer(serializers.Serializer): + """Serializer for BackOffice role tab.""" + role_name = serializers.CharField() + role_counter = serializers.IntegerField() diff --git a/apps/account/urls/back.py b/apps/account/urls/back.py index b76e8110..3635c1a6 100644 --- a/apps/account/urls/back.py +++ b/apps/account/urls/back.py @@ -7,6 +7,7 @@ app_name = 'account' urlpatterns = [ path('role/', views.RoleListView.as_view(), name='role-list-create'), + path('role-tab/', views.RoleTabRetrieveView.as_view(), name='role-tab'), path('user-role/', views.UserRoleListView.as_view(), name='user-role-list-create'), path('user/', views.UserListView.as_view(), name='user-create-list'), path('user//', views.UserRUDView.as_view(), name='user-rud'), diff --git a/apps/account/views/back.py b/apps/account/views/back.py index 09ab1048..94d32a14 100644 --- a/apps/account/views/back.py +++ b/apps/account/views/back.py @@ -1,11 +1,12 @@ from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import generics, permissions +from rest_framework import generics, permissions, status +from rest_framework.response import Response from rest_framework.filters import OrderingFilter import csv from django.http import HttpResponse, HttpResponseNotFound from rest_framework.authtoken.models import Token -from account import models +from account import models, filters from account.models import User from account.serializers import back as serializers from account.serializers.common import RoleBaseSerializer @@ -16,6 +17,34 @@ class RoleListView(generics.ListCreateAPIView): queryset = models.Role.objects.all() +class RoleTabRetrieveView(generics.GenericAPIView): + permission_classes = [permissions.IsAdminUser] + + def get_queryset(self): + """Overridden get_queryset method.""" + additional_filters = {} + + if self.request.user.userrole_set.country_admin_role().exists(): + additional_filters.update({'userrole__country__code': self.request.country_code}) + + return models.Role.objects.filter(**additional_filters)\ + .annotate_role_name()\ + .values('role_name')\ + .annotate_role_counter()\ + .values('role_name', 'role_counter') + + def get(self, request, *args, **kwargs): + """Implement GET-method""" + data = list(self.get_queryset()) + + # todo: Need refactoring. Extend data list with non-existed role. + for role in models.Role.role_names(): + if role not in [role.get('role_name') for role in data]: + data.append({'role_name': role, 'role_number': 0}) + + return Response(data, status=status.HTTP_200_OK) + + class UserRoleListView(generics.ListCreateAPIView): serializer_class = serializers.UserRoleSerializer queryset = models.UserRole.objects.all() @@ -26,21 +55,14 @@ class UserListView(generics.ListCreateAPIView): queryset = User.objects.prefetch_related('roles') serializer_class = serializers.BackUserSerializer permission_classes = (permissions.IsAdminUser,) - filter_backends = (DjangoFilterBackend, OrderingFilter) + filter_class = filters.AccountBackOfficeFilter + filter_backends = (OrderingFilter, ) - filterset_fields = ( - 'email_confirmed', - 'is_staff', - 'is_active', - 'is_superuser', - 'roles', - ) ordering_fields = ( 'email_confirmed', 'is_staff', 'is_active', 'is_superuser', - 'roles', 'last_login', 'date_joined', ) diff --git a/apps/tag/filters.py b/apps/tag/filters.py index 7d82bf84..a9f675c6 100644 --- a/apps/tag/filters.py +++ b/apps/tag/filters.py @@ -5,6 +5,7 @@ from django.conf import settings from tag import models from product import models as product_models + class TagsBaseFilterSet(filters.FilterSet): # Object type choices From 8544098f8cdeb3422575454d501b025bdae4a657 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 17 Jan 2020 17:41:42 +0300 Subject: [PATCH 095/119] fix command #2 --- .../management/commands/update_establishment_image_urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/establishment/management/commands/update_establishment_image_urls.py b/apps/establishment/management/commands/update_establishment_image_urls.py index 397d4f0d..3256f18b 100644 --- a/apps/establishment/management/commands/update_establishment_image_urls.py +++ b/apps/establishment/management/commands/update_establishment_image_urls.py @@ -34,7 +34,7 @@ class Command(BaseCommand): task = update_establishment_image_urls.s( (index + bucket_size) / bucket_size, summary_tasks, - bucket.values_list('id', flat=True) + list(bucket.values_list('id', flat=True)) ) tasks.append(task) From 76612238cdacf413c179fee4a2be4ffd01041f83 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 17 Jan 2020 18:11:56 +0300 Subject: [PATCH 096/119] updated transfer strategy --- make_data_migration.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/make_data_migration.sh b/make_data_migration.sh index edc50180..a6fd811d 100755 --- a/make_data_migration.sh +++ b/make_data_migration.sh @@ -30,4 +30,5 @@ ./manage.py transfer --transfer_text_review # оптимизация изображений -/manage.py news_optimize_images # сжимает картинки в описаниях новостей \ No newline at end of file +/manage.py news_optimize_images # сжимает картинки в описаниях новостей +/manage.py update_establishment_image_urls # удаляет неотображаемые картинки из модели заведения \ No newline at end of file From f400ceed8157666d8b15e0edb7a3ba381f3a742d Mon Sep 17 00:00:00 2001 From: Dmitry Kuzmenko Date: Fri, 17 Jan 2020 18:15:21 +0300 Subject: [PATCH 097/119] add filter backend for user --- apps/account/views/back.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/account/views/back.py b/apps/account/views/back.py index 94d32a14..1a411e9b 100644 --- a/apps/account/views/back.py +++ b/apps/account/views/back.py @@ -56,7 +56,7 @@ class UserListView(generics.ListCreateAPIView): serializer_class = serializers.BackUserSerializer permission_classes = (permissions.IsAdminUser,) filter_class = filters.AccountBackOfficeFilter - filter_backends = (OrderingFilter, ) + filter_backends = (OrderingFilter, DjangoFilterBackend) ordering_fields = ( 'email_confirmed', From a05db696b01f46418c2e82a026c938b2f5a3f16d Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 17 Jan 2020 19:08:15 +0300 Subject: [PATCH 098/119] optimize db queries on /api/back/establishments/employees --- apps/establishment/models.py | 9 +++++++++ apps/establishment/serializers/back.py | 16 ++++++++++++++++ apps/establishment/views/back.py | 13 +++---------- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 5f8ec73d..e21c95a6 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1063,6 +1063,15 @@ class EmployeeQuerySet(models.QuerySet): def with_extended_related(self): return self.prefetch_related('establishments') + def with_back_office_related(self): + return self.prefetch_related( + Prefetch('establishmentemployee_set', + queryset=EstablishmentEmployee.objects.actual() + .prefetch_related('establishment', 'position').order_by('-from_date'), + to_attr='prefetched_establishment_employee'), + 'awards' + ) + class Employee(BaseAttributes): """Employee model.""" diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 8f9631f1..41ddc6d5 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -247,12 +247,18 @@ class EmployeeBackSerializers(serializers.ModelSerializer): def get_public_mark(self, obj): """Get last list actual public_mark""" + if hasattr(obj, 'prefetched_establishment_employee'): + return obj.prefetched_establishment_employee[0].establishment.public_mark if len( + obj.prefetched_establishment_employee) else None qs = self.get_qs(obj) if qs: return qs.public_mark return None def get_toque_number(self, obj): + if hasattr(obj, 'prefetched_establishment_employee'): + return obj.prefetched_establishment_employee[0].establishment.toque_number if len( + obj.prefetched_establishment_employee) else None qs = self.get_qs(obj) if qs: return qs.toque_number @@ -260,6 +266,11 @@ class EmployeeBackSerializers(serializers.ModelSerializer): def get_positions(self, obj): """Get last list actual positions""" + if hasattr(obj, 'prefetched_establishment_employee'): + if not len(obj.prefetched_establishment_employee): + return [] + return [PositionBackSerializer(ee.position).data for ee in obj.prefetched_establishment_employee] + est_id = self.get_qs(obj) if not est_id: @@ -275,6 +286,11 @@ class EmployeeBackSerializers(serializers.ModelSerializer): def get_establishment(self, obj): """Get last actual establishment""" + if hasattr(obj, 'prefetched_establishment_employee'): + return { + 'id': obj.prefetched_establishment_employee[0].establishment.pk, + 'slug': obj.prefetched_establishment_employee[0].establishment.slug, + } if len(obj.prefetched_establishment_employee) else None est = self.get_qs(obj) if not est: diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index f98747b6..5ef8fff3 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -174,21 +174,14 @@ class EmployeeListCreateView(generics.ListCreateAPIView): permission_classes = (permissions.AllowAny,) filter_class = filters.EmployeeBackFilter serializer_class = serializers.EmployeeBackSerializers - queryset = models.Employee.objects.all().prefetch_related( - 'establishmentemployee_set', - 'establishmentemployee_set__establishment', - 'awards', - ) + queryset = models.Employee.objects.all().with_back_office_related() class EmployeesListSearchViews(generics.ListAPIView): """Employee search view""" pagination_class = None permission_classes = (permissions.AllowAny,) - queryset = models.Employee.objects.all().prefetch_related( - 'establishmentemployee_set', - 'establishmentemployee_set__establishment', - ).select_related('photo') + queryset = models.Employee.objects.all().with_back_office_related().select_related('photo') filter_class = filters.EmployeeBackSearchFilter serializer_class = serializers.EmployeeBackSerializers @@ -207,7 +200,7 @@ class EstablishmentEmployeeListView(generics.ListCreateAPIView): class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): """Employee RUD view.""" serializer_class = serializers.EmployeeBackSerializers - queryset = models.Employee.objects.all() + queryset = models.Employee.objects.all().with_back_office_related() class EstablishmentTypeListCreateView(generics.ListCreateAPIView): From 249f6f709e769feaf15eaba06882bc054de57013 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 17 Jan 2020 19:13:29 +0300 Subject: [PATCH 099/119] Revert "Added type for pop filter in tags" This reverts commit 2b28a56 --- apps/tag/views.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/apps/tag/views.py b/apps/tag/views.py index e0b19fa5..c621cd71 100644 --- a/apps/tag/views.py +++ b/apps/tag/views.py @@ -129,19 +129,10 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): result_list.append(wine_regions) - item_types = { - 'pop': 'pop' - } - for item in result_list: if 'filters' in item: item['filters'].sort(key=lambda x: x.get('label_translated')) - index_name = item.get('index_name') - - if index_name in item_types: - item['type'] = item_types[index_name] - if filter_flags['toque_number']: toques = { "index_name": "toque_number", From 918c1fa17db45c96a0d3e6a36bf5b1600f96cdf7 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 17 Jan 2020 20:21:59 +0300 Subject: [PATCH 100/119] middleware to log db queries --- apps/utils/middleware.py | 28 ++++++++++++++++++++++++++++ project/settings/development.py | 5 ++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/apps/utils/middleware.py b/apps/utils/middleware.py index c772e645..f5048345 100644 --- a/apps/utils/middleware.py +++ b/apps/utils/middleware.py @@ -1,5 +1,8 @@ """Custom middlewares.""" +import logging + from django.utils import translation, timezone +from django.db import connection from account.models import User from configuration.models import TranslationSettings @@ -7,6 +10,8 @@ from main.methods import determine_user_city from main.models import SiteSettings from translation.models import Language +logger = logging.getLogger(__name__) + def get_locale(cookie_dict): return cookie_dict.get('locale') @@ -92,3 +97,26 @@ def user_last_ip(get_response): return response return middleware + + +def log_db_queries_per_API_request(get_response): + """Middleware-helper to optimize requests performance""" + + def middleware(request): + total_time = 0 + response = get_response(request) + for query in connection.queries: + query_time = query.get('time') + if query_time is None: + query_time = query.get('duration', 0) / 1000 + total_time += float(query_time) + + total_queries = len(connection.queries) + if total_queries > 10: + logger.error( + f'\t{len(connection.queries)} queries run, total {total_time} seconds \t' + f'URL: "{request.method} {request.get_full_path_info()}\t"' + ) + return response + + return middleware diff --git a/project/settings/development.py b/project/settings/development.py index 24a1a795..3048541b 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -80,4 +80,7 @@ EMAIL_USE_TLS = True EMAIL_HOST = 'smtp.gmail.com' EMAIL_HOST_USER = 'anatolyfeteleu@gmail.com' EMAIL_HOST_PASSWORD = 'nggrlnbehzksgmbt' -EMAIL_PORT = 587 \ No newline at end of file +EMAIL_PORT = 587 + + +MIDDLEWARE.append('utils.middleware.log_db_queries_per_API_request') From 17d8db649b8ea71389c3be73bc992db31acbb80f Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 17 Jan 2020 20:23:49 +0300 Subject: [PATCH 101/119] middleware to log db queries #2 --- apps/utils/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/utils/middleware.py b/apps/utils/middleware.py index f5048345..7ab5f532 100644 --- a/apps/utils/middleware.py +++ b/apps/utils/middleware.py @@ -115,7 +115,7 @@ def log_db_queries_per_API_request(get_response): if total_queries > 10: logger.error( f'\t{len(connection.queries)} queries run, total {total_time} seconds \t' - f'URL: "{request.method} {request.get_full_path_info()}\t"' + f'URL: "{request.method} {request.get_full_path_info()}"' ) return response From be96a6c7ba634640a5d814ab0a4b8a912e32423e Mon Sep 17 00:00:00 2001 From: dormantman Date: Fri, 17 Jan 2020 17:29:58 +0300 Subject: [PATCH 102/119] Added type for pop filter in tags --- apps/tag/views.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/tag/views.py b/apps/tag/views.py index c621cd71..f526b572 100644 --- a/apps/tag/views.py +++ b/apps/tag/views.py @@ -129,10 +129,19 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): result_list.append(wine_regions) + item_types = { + 'pop': 'pop' + } + for item in result_list: if 'filters' in item: item['filters'].sort(key=lambda x: x.get('label_translated')) + index_name = item.get('index_name') + + if index_name in item_types: + item['type'] = item_types[index_name] + if filter_flags['toque_number']: toques = { "index_name": "toque_number", @@ -206,7 +215,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): result_list = [category for category in result_list if category.get('index_name') != 'tag'] if len(tag_category): tag_category = list(filter(lambda x: x.get('index_name') == 'pop', tag_category[0]['filters'])) - if len(tag_category): # we have Pop tag in our results + if len(tag_category): # we have Pop tag in our results tag_category = tag_category[0] tag_category['param_name'] = 'tags_id__in' result_list.append(tag_category) From 51114b0e9564c46cb5ad5b5a4d36d6cb591344f5 Mon Sep 17 00:00:00 2001 From: dormantman Date: Sat, 18 Jan 2020 01:04:30 +0300 Subject: [PATCH 103/119] Added db index to siteinterfacedictionary text for optimize --- .../migrations/0008_index_siteifdict_text.py | 18 ++++++++++++++++++ apps/translation/models.py | 4 ++++ 2 files changed, 22 insertions(+) create mode 100644 apps/translation/migrations/0008_index_siteifdict_text.py diff --git a/apps/translation/migrations/0008_index_siteifdict_text.py b/apps/translation/migrations/0008_index_siteifdict_text.py new file mode 100644 index 00000000..b5b485ec --- /dev/null +++ b/apps/translation/migrations/0008_index_siteifdict_text.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.7 on 2020-01-17 22:01 + +import django.contrib.postgres.indexes +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('translation', '0007_language_is_active'), + ] + + operations = [ + migrations.AddIndex( + model_name='siteinterfacedictionary', + index=django.contrib.postgres.indexes.GinIndex(fields=['text'], name='translation_text_0b2bfa_gin'), + ), + ] diff --git a/apps/translation/models.py b/apps/translation/models.py index a288b334..86e5e52a 100644 --- a/apps/translation/models.py +++ b/apps/translation/models.py @@ -1,5 +1,6 @@ """Translation app models.""" from django.contrib.postgres.fields import JSONField +from django.contrib.postgres.indexes import GinIndex from django.db import models from django.utils.translation import gettext_lazy as _ from django.apps import apps @@ -105,6 +106,9 @@ class SiteInterfaceDictionary(ProjectBaseMixin): verbose_name = _('Site interface dictionary') verbose_name_plural = _('Site interface dictionary') + indexes = [ + GinIndex(fields=['text']) + ] def __str__(self): return f'{self.page}: {self.keywords}' From f9d4d4ff575f432a4333906133391210853b88fe Mon Sep 17 00:00:00 2001 From: dormantman Date: Sat, 18 Jan 2020 01:05:44 +0300 Subject: [PATCH 104/119] Revert "Added type for pop filter in tags" This reverts commit be96a6c7 --- apps/tag/views.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/apps/tag/views.py b/apps/tag/views.py index f526b572..c621cd71 100644 --- a/apps/tag/views.py +++ b/apps/tag/views.py @@ -129,19 +129,10 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): result_list.append(wine_regions) - item_types = { - 'pop': 'pop' - } - for item in result_list: if 'filters' in item: item['filters'].sort(key=lambda x: x.get('label_translated')) - index_name = item.get('index_name') - - if index_name in item_types: - item['type'] = item_types[index_name] - if filter_flags['toque_number']: toques = { "index_name": "toque_number", @@ -215,7 +206,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet): result_list = [category for category in result_list if category.get('index_name') != 'tag'] if len(tag_category): tag_category = list(filter(lambda x: x.get('index_name') == 'pop', tag_category[0]['filters'])) - if len(tag_category): # we have Pop tag in our results + if len(tag_category): # we have Pop tag in our results tag_category = tag_category[0] tag_category['param_name'] = 'tags_id__in' result_list.append(tag_category) From e6dfa6d74d9f528fdf045e62daf45ccc252c0147 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 20 Jan 2020 11:21:51 +0300 Subject: [PATCH 105/119] fix SiteAffilation roles transfer --- apps/account/management/commands/add_affilations.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/account/management/commands/add_affilations.py b/apps/account/management/commands/add_affilations.py index 591e676d..c59aaafe 100644 --- a/apps/account/management/commands/add_affilations.py +++ b/apps/account/management/commands/add_affilations.py @@ -70,10 +70,12 @@ class Command(BaseCommand): role_choice = getattr(Role, old_role.new_role) sites = SiteSettings.objects.filter(old_id=s.site_id) for site in sites: - role = Role.objects.filter(site=site, role=role_choice) + data = {'site': site, 'role': role_choice} + + role = Role.objects.filter(**data) if not role.exists(): objects.append( - Role(site=site, role=role_choice) + Role(**data) ) Role.objects.bulk_create(objects) @@ -81,7 +83,7 @@ class Command(BaseCommand): def update_site_role(self): roles = Role.objects.filter(country__isnull=True).select_related('site')\ - .filter(site__id__isnull=False).select_for_update() + .filter(site__id__isnull=False).select_for_update() with transaction.atomic(): for role in tqdm(roles, desc='Update role country'): role.country = role.site.country @@ -114,8 +116,7 @@ class Command(BaseCommand): users = User.objects.filter(old_id=s.account_id) for user in users: for role in roles: - user_role = UserRole.objects.get_or_create(user=user, - role=role) + UserRole.objects.get_or_create(user=user, role=role, state=UserRole.VALIDATED) self.stdout.write(self.style.WARNING(f'Added users roles.')) def superuser_role_sql(self): From 765a66dc6892b2e5ac42510fc24483a5dc60cde8 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 20 Jan 2020 11:43:35 +0300 Subject: [PATCH 106/119] fix role counter --- apps/account/views/back.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/account/views/back.py b/apps/account/views/back.py index 1a411e9b..628dc486 100644 --- a/apps/account/views/back.py +++ b/apps/account/views/back.py @@ -24,8 +24,9 @@ class RoleTabRetrieveView(generics.GenericAPIView): """Overridden get_queryset method.""" additional_filters = {} - if self.request.user.userrole_set.country_admin_role().exists(): - additional_filters.update({'userrole__country__code': self.request.country_code}) + if (self.request.user.userrole_set.country_admin_role().exists() and + hasattr(self.request.user, 'country_code')): + additional_filters.update({'country__code': self.request.country_code}) return models.Role.objects.filter(**additional_filters)\ .annotate_role_name()\ From 452c6f22aebf095aeebf179115d23367c3b3e8b2 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 20 Jan 2020 11:45:26 +0300 Subject: [PATCH 107/119] fix role counter --- apps/account/views/back.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/account/views/back.py b/apps/account/views/back.py index 628dc486..1a3f5ccd 100644 --- a/apps/account/views/back.py +++ b/apps/account/views/back.py @@ -25,7 +25,7 @@ class RoleTabRetrieveView(generics.GenericAPIView): additional_filters = {} if (self.request.user.userrole_set.country_admin_role().exists() and - hasattr(self.request.user, 'country_code')): + hasattr(self.request, 'country_code')): additional_filters.update({'country__code': self.request.country_code}) return models.Role.objects.filter(**additional_filters)\ From 215e7b5f19cbedca8df758c9f773c4fa300d85d2 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 20 Jan 2020 12:13:50 +0300 Subject: [PATCH 108/119] establishment employee list --- apps/establishment/serializers/back.py | 42 +++++++++++++++++++++++++- apps/establishment/views/back.py | 6 ++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 41ddc6d5..05cccc56 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -7,7 +7,7 @@ from rest_framework import serializers from account.serializers.common import UserShortSerializer from establishment import models from establishment import serializers as model_serializers -from establishment.models import ContactPhone +from establishment.models import ContactPhone, EstablishmentEmployee from gallery.models import Image from location.models import Address from location.serializers import AddressDetailSerializer, TranslatedField @@ -340,6 +340,46 @@ class EstablishmentEmployeeBackSerializer(serializers.ModelSerializer): ] +class EstEmployeeBackSerializer(EmployeeBackSerializers): + def get_positions(self, obj): + es_emp = EstablishmentEmployee.objects.filter( + employee=obj + ).distinct().order_by('position_id') + result = [] + for item in es_emp: + result.append({ + 'id': item.id, + 'from_date': item.from_date, + 'to_date': item.to_date, + 'status': item.status, + 'position_id': item.position_id, + 'position_priority': item.position.priority, + 'position_index_name': item.position.index_name, + 'position_name_translated': item.position.name_translated, + }) + + return result + + class Meta: + model = models.Employee + fields = [ + 'id', + 'name', + 'last_name', + 'user', + 'public_mark', + 'positions', + 'awards', + 'sex', + 'birth_date', + 'email', + 'phone', + 'toque_number', + 'available_for_events', + 'photo', + ] + + class EstablishmentBackOfficeGallerySerializer(serializers.ModelSerializer): """Serializer class for model EstablishmentGallery.""" diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 5ef8fff3..8b446ac6 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -189,12 +189,14 @@ class EmployeesListSearchViews(generics.ListAPIView): class EstablishmentEmployeeListView(generics.ListCreateAPIView): """Establishment emplyoees list view.""" permission_classes = (permissions.AllowAny,) - serializer_class = serializers.EstablishmentEmployeeBackSerializer + serializer_class = serializers.EstEmployeeBackSerializer pagination_class = None def get_queryset(self): establishment_id = self.kwargs['establishment_id'] - return models.EstablishmentEmployee.objects.filter(establishment__id=establishment_id).order_by('position') + return models.Employee.objects.filter( + establishmentemployee__establishment_id=establishment_id, + ).distinct() class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): From 5a662fd745f65e477a7c508cc2c2d272d0c3139f Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 20 Jan 2020 13:18:41 +0300 Subject: [PATCH 109/119] Image id on city creation --- apps/location/serializers/common.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index e466e62f..ad60a7a2 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -3,6 +3,7 @@ from django.contrib.gis.geos import Point from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from location import models +from gallery import models as gallery_models from utils.serializers import TranslatedField, ImageBaseSerializer @@ -83,6 +84,11 @@ class CityBaseSerializer(serializers.ModelSerializer): queryset=models.Country.objects.all(), write_only=True ) + image_id = serializers.PrimaryKeyRelatedField( + source='image', + queryset=gallery_models.Image.objects.all(), + write_only=True + ) country = CountrySerializer(read_only=True) class Meta: @@ -97,9 +103,10 @@ class CityBaseSerializer(serializers.ModelSerializer): 'postal_code', 'is_island', 'image', + 'image_id', ] extra_fields = { - 'image': {'write_only': True} + 'image': {'read_only': True} } From bb8accefbf0e91b1cfff985e4f7140030390df3b Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Mon, 20 Jan 2020 10:25:09 +0000 Subject: [PATCH 110/119] optimize fix --- .../commands/collection_optimize_images.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/apps/collection/management/commands/collection_optimize_images.py b/apps/collection/management/commands/collection_optimize_images.py index 2b6c9e3d..6da4c04a 100644 --- a/apps/collection/management/commands/collection_optimize_images.py +++ b/apps/collection/management/commands/collection_optimize_images.py @@ -11,19 +11,26 @@ class Command(BaseCommand): SORL_THUMBNAIL_ALIAS = 'collection_image' def handle(self, *args, **options): + max_size = 1048576 + with transaction.atomic(): for collection in Collection.objects.all(): if not image_url_valid(collection.image_url): continue - _, width, height = get_image_meta_by_url(collection.image_url) - sorl_settings = settings.SORL_THUMBNAIL_ALIASES[self.SORL_THUMBNAIL_ALIAS] - sorl_width_height = sorl_settings['geometry_string'].split('x') + size, width, height = get_image_meta_by_url(collection.image_url) - if int(sorl_width_height[0]) > width or int(sorl_width_height[1]) > height: - collection.image_url = get_thumbnail( - file_=collection.image_url, - **settings.SORL_THUMBNAIL_ALIASES[self.SORL_THUMBNAIL_ALIAS] - ).url + if size < max_size: + self.stdout.write(self.style.SUCCESS(f'No need to compress images size is {size / (2 ** 20)}Mb\n')) + continue + + percents = round(max_size / (size * 0.01)) + width = round(width * percents / 100) + height = round(height * percents / 100) + collection.image_url = get_thumbnail( + file_=collection.image_url, + geometry_string=f'{width}x{height}', + upscale=False + ).url collection.save() From 20a96e1e51badc9b77528915aca74c0656009cfe Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Mon, 20 Jan 2020 10:40:34 +0000 Subject: [PATCH 111/119] optimize news images --- apps/news/management/commands/news_optimize_images.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/news/management/commands/news_optimize_images.py b/apps/news/management/commands/news_optimize_images.py index 440afbc2..8b99c774 100644 --- a/apps/news/management/commands/news_optimize_images.py +++ b/apps/news/management/commands/news_optimize_images.py @@ -1,6 +1,7 @@ # coding=utf-8 from django.core.management.base import BaseCommand +from project.settings import SCHEMA_URI, SITE_DOMAIN_URI from utils.methods import get_url_images_in_text, get_image_meta_by_url from news.models import News from sorl.thumbnail import get_thumbnail @@ -29,6 +30,11 @@ class Command(BaseCommand): def optimize(self, text, max_size, max_quality): """optimize news images""" for image in get_url_images_in_text(text): + if not image.startswith('http'): + image = f'{SCHEMA_URI}://{SITE_DOMAIN_URI}{image}' + + self.stdout.write(self.style.NOTICE(f'{image}')) + try: size, width, height = get_image_meta_by_url(image) except IOError as ie: From df601e323437dc683c0940463e79652a0d0327b3 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Mon, 20 Jan 2020 10:42:16 +0000 Subject: [PATCH 112/119] optimize news images --- apps/news/management/commands/news_optimize_images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/news/management/commands/news_optimize_images.py b/apps/news/management/commands/news_optimize_images.py index 8b99c774..63906037 100644 --- a/apps/news/management/commands/news_optimize_images.py +++ b/apps/news/management/commands/news_optimize_images.py @@ -33,7 +33,7 @@ class Command(BaseCommand): if not image.startswith('http'): image = f'{SCHEMA_URI}://{SITE_DOMAIN_URI}{image}' - self.stdout.write(self.style.NOTICE(f'{image}')) + self.stdout.write(self.style.SUCCESS(f'{image} {text}')) try: size, width, height = get_image_meta_by_url(image) From a9c1e5b154382bb5ab7377c945eb6b475b7b9cc8 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 20 Jan 2020 13:51:18 +0300 Subject: [PATCH 113/119] in favorites for news --- apps/news/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index db1d8fd5..672334dd 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -161,6 +161,7 @@ class NewsDetailWebSerializer(NewsDetailSerializer): should_read = SerializerMethodField() agenda = AgendaSerializer() banner = NewsBannerSerializer() + in_favorites = serializers.BooleanField(read_only=True) class Meta(NewsDetailSerializer.Meta): """Meta class.""" @@ -170,6 +171,7 @@ class NewsDetailWebSerializer(NewsDetailSerializer): 'should_read', 'agenda', 'banner', + 'in_favorites', ) def get_same_theme(self, obj): From a31763c55062270b32a69d2feb8a215a5bc257f3 Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Mon, 20 Jan 2020 10:58:00 +0000 Subject: [PATCH 114/119] fix regexp for find images --- apps/news/management/commands/news_optimize_images.py | 6 ------ apps/utils/methods.py | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/news/management/commands/news_optimize_images.py b/apps/news/management/commands/news_optimize_images.py index 63906037..440afbc2 100644 --- a/apps/news/management/commands/news_optimize_images.py +++ b/apps/news/management/commands/news_optimize_images.py @@ -1,7 +1,6 @@ # coding=utf-8 from django.core.management.base import BaseCommand -from project.settings import SCHEMA_URI, SITE_DOMAIN_URI from utils.methods import get_url_images_in_text, get_image_meta_by_url from news.models import News from sorl.thumbnail import get_thumbnail @@ -30,11 +29,6 @@ class Command(BaseCommand): def optimize(self, text, max_size, max_quality): """optimize news images""" for image in get_url_images_in_text(text): - if not image.startswith('http'): - image = f'{SCHEMA_URI}://{SITE_DOMAIN_URI}{image}' - - self.stdout.write(self.style.SUCCESS(f'{image} {text}')) - try: size, width, height = get_image_meta_by_url(image) except IOError as ie: diff --git a/apps/utils/methods.py b/apps/utils/methods.py index b9855545..a8f5171e 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -207,7 +207,7 @@ def section_name_into_index_name(section_name: str): def get_url_images_in_text(text): """Find images urls in text""" - return re.findall(r'[^\"\'=\s]+\.jpe?g|png|gif|svg', text) + return re.findall(r'\', text) def get_image_meta_by_url(url) -> (int, int, int): From c0b517e5fe594050ad7ab6c8815089ea352ff5ef Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Mon, 20 Jan 2020 11:04:02 +0000 Subject: [PATCH 115/119] find image regexp --- apps/utils/methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/utils/methods.py b/apps/utils/methods.py index a8f5171e..58cc3f52 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -207,7 +207,7 @@ def section_name_into_index_name(section_name: str): def get_url_images_in_text(text): """Find images urls in text""" - return re.findall(r'\', text) + return re.findall(r'\', text) def get_image_meta_by_url(url) -> (int, int, int): From 9ce5f89d74bc47a558f5550a73d3070143345714 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 20 Jan 2020 14:18:56 +0300 Subject: [PATCH 116/119] try to fix issue w/ news images optimization --- .../commands/news_optimize_images.py | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/apps/news/management/commands/news_optimize_images.py b/apps/news/management/commands/news_optimize_images.py index 440afbc2..011bf6ae 100644 --- a/apps/news/management/commands/news_optimize_images.py +++ b/apps/news/management/commands/news_optimize_images.py @@ -28,29 +28,30 @@ class Command(BaseCommand): def optimize(self, text, max_size, max_quality): """optimize news images""" - for image in get_url_images_in_text(text): - try: - size, width, height = get_image_meta_by_url(image) - except IOError as ie: - self.stdout.write(self.style.NOTICE(f'{ie}\n')) - continue + if isinstance(text, str): + for image in get_url_images_in_text(text): + try: + size, width, height = get_image_meta_by_url(image) + except IOError as ie: + self.stdout.write(self.style.NOTICE(f'{ie}\n')) + continue - if size < max_size: - self.stdout.write(self.style.SUCCESS(f'No need to compress images size is {size / (2**20)}Mb\n')) - continue + if size < max_size: + self.stdout.write(self.style.SUCCESS(f'No need to compress images size is {size / (2**20)}Mb\n')) + continue - percents = round(max_size / (size * 0.01)) - width = round(width * percents / 100) - height = round(height * percents / 100) - optimized_image = get_thumbnail( - file_=image, - geometry_string=f'{width}x{height}', - upscale=False, - quality=max_quality - ).url - text = text.replace(image, optimized_image) - self.stdout.write(self.style.SUCCESS(f'Optimized {image} -> {optimized_image}\n' - f'Quality [{percents}%]\n')) + percents = round(max_size / (size * 0.01)) + width = round(width * percents / 100) + height = round(height * percents / 100) + optimized_image = get_thumbnail( + file_=image, + geometry_string=f'{width}x{height}', + upscale=False, + quality=max_quality + ).url + text = text.replace(image, optimized_image) + self.stdout.write(self.style.SUCCESS(f'Optimized {image} -> {optimized_image}\n' + f'Quality [{percents}%]\n')) return text From beefc82d7b1828cf00565260fb2940e42b0b5c30 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 20 Jan 2020 14:20:46 +0300 Subject: [PATCH 117/119] toque number and employee for establishment --- apps/establishment/filters.py | 8 +++++++- apps/establishment/models.py | 10 ++++++++-- apps/establishment/serializers/back.py | 9 ++++++++- make_data_migration.sh | 8 +++++++- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index 14d6ae05..3a7d3398 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -69,7 +69,7 @@ class EmployeeBackFilter(filters.FilterSet): search = filters.CharFilter(method='search_by_name_or_last_name') position_id = filters.NumberFilter(method='search_by_actual_position_id') public_mark = filters.NumberFilter(method='search_by_public_mark') - toque_number = filters.NumberFilter(field_name='toque_number') + toque_number = filters.NumberFilter(method='search_by_toque_number') username = filters.CharFilter(method='search_by_username_or_name') class Meta: @@ -102,6 +102,12 @@ class EmployeeBackFilter(filters.FilterSet): return queryset.search_by_public_mark(value) return queryset + def search_by_toque_number(self, queryset, name, value): + """Search by establishment toque_number.""" + if value not in EMPTY_VALUES: + return queryset.search_by_toque_number(value) + return queryset + def search_by_username_or_name(self, queryset, name, value): """Search by username or name.""" if value not in EMPTY_VALUES: diff --git a/apps/establishment/models.py b/apps/establishment/models.py index e21c95a6..7f20c529 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1036,16 +1036,22 @@ class EmployeeQuerySet(models.QuerySet): def search_by_position_id(self, value): """Search by position_id.""" - return self.search_by_actual_employee().filter( + return self.filter( Q(establishmentemployee__position_id=value), ) def search_by_public_mark(self, value): """Search by establishment public_mark.""" - return self.search_by_actual_employee().filter( + return self.filter( Q(establishmentemployee__establishment__public_mark=value), ) + def search_by_toque_number(self, value): + """Search by establishment toque_number.""" + return self.filter( + Q(establishmentemployee__establishment__toque_number=value), + ) + def search_by_username_or_name(self, value): """Search by username or name.""" return self.search_by_actual_employee().filter( diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 05cccc56..be59c73c 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -341,9 +341,16 @@ class EstablishmentEmployeeBackSerializer(serializers.ModelSerializer): class EstEmployeeBackSerializer(EmployeeBackSerializers): + @property + def request_kwargs(self): + """Get url kwargs from request.""" + return self.context.get('request').parser_context.get('kwargs') + def get_positions(self, obj): + establishment_id = self.request_kwargs.get('establishment_id') es_emp = EstablishmentEmployee.objects.filter( - employee=obj + employee=obj, + establishment_id=establishment_id, ).distinct().order_by('position_id') result = [] for item in es_emp: diff --git a/make_data_migration.sh b/make_data_migration.sh index a6fd811d..d5edc793 100755 --- a/make_data_migration.sh +++ b/make_data_migration.sh @@ -31,4 +31,10 @@ # оптимизация изображений /manage.py news_optimize_images # сжимает картинки в описаниях новостей -/manage.py update_establishment_image_urls # удаляет неотображаемые картинки из модели заведения \ No newline at end of file +/manage.py update_establishment_image_urls # удаляет неотображаемые картинки из модели заведения + +# сотрудники с позициями для заведений +./manage.py add_employee +./manage.py add_position +./manage.py add_empl_position +./manage.py update_employee \ No newline at end of file From f026cfcebae31b1278dce3085bba0fd1a97a113e Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 20 Jan 2020 14:52:22 +0300 Subject: [PATCH 118/119] fix rud for inactive news --- apps/news/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/news/models.py b/apps/news/models.py index 3be956a7..fffed321 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -7,6 +7,7 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import HStoreField from django.db import models from django.db.models import Case, When +from django.urls.exceptions import NoReverseMatch from django.utils import timezone from django.utils.translation import gettext_lazy as _ from rest_framework.reverse import reverse @@ -295,7 +296,10 @@ class News(GalleryMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin, @property def web_url(self): - return reverse('web:news:rud', kwargs={'slug': next(iter(self.slugs.values()))}) + try: + return reverse('web:news:rud', kwargs={'slug': next(iter(self.slugs.values()))}) + except NoReverseMatch as e: + return None # no active links def should_read(self, user): return self.__class__.objects.should_read(self, user)[:3] From a994e7214bbf01de2032f7b74c40edfe2467b486 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 20 Jan 2020 15:51:47 +0300 Subject: [PATCH 119/119] fix from_date and to_date for est_employee --- apps/establishment/serializers/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 473a2b35..066452a7 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -202,7 +202,7 @@ class EstablishmentEmployeeCreateSerializer(serializers.ModelSerializer): """Meta class.""" model = models.EstablishmentEmployee - fields = ('id',) + fields = ('id', 'from_date', 'to_date') def _validate_entity(self, entity_id_param: str, entity_class): entity_id = self.context.get('request').parser_context.get('kwargs').get(entity_id_param)