From ce41b96ec3f77c735c41173131d83e5604729acd Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Fri, 22 Nov 2019 12:34:11 +0300 Subject: [PATCH 01/26] update dev settings --- project/settings/development.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/project/settings/development.py b/project/settings/development.py index 06f1199b..057438f5 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -18,6 +18,17 @@ SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm.id-east.ru' +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + }, + 'es_queue': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'redis://localhost:6379/2' + } +} + + # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { From 00d7bbe7ec95f39bdca5fc4702e90501875f7218 Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Fri, 22 Nov 2019 12:45:14 +0300 Subject: [PATCH 02/26] update crontab parameters --- apps/search_indexes/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/tasks.py b/apps/search_indexes/tasks.py index 4935814d..b9fefff7 100644 --- a/apps/search_indexes/tasks.py +++ b/apps/search_indexes/tasks.py @@ -13,7 +13,7 @@ from product.models import Product logger = logging.getLogger(__name__) -@periodic_task(run_every=crontab(minute=1)) +@periodic_task(run_every=crontab(minute='*/1')) def update_index(): """Updates ES index.""" try: From f7703f18b05cc357c47ef134c2284e6c7d93c558 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 22 Nov 2019 13:02:04 +0300 Subject: [PATCH 03/26] refactored upload image endpoint --- apps/gallery/serializers.py | 54 +++++++++++++++++++++++++++++++++++++ apps/utils/models.py | 12 +++++++++ project/settings/base.py | 7 +++++ 3 files changed, 73 insertions(+) diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index e817cbd8..1f96dca8 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -1,4 +1,8 @@ +from django.conf import settings +from django.core.validators import MinValueValidator, MaxValueValidator from rest_framework import serializers +from sorl.thumbnail.parsers import parse_crop +from sorl.thumbnail.parsers import ThumbnailParseError from . import models @@ -8,10 +12,21 @@ class ImageSerializer(serializers.ModelSerializer): # REQUEST file = serializers.ImageField(source='image', write_only=True) + width = serializers.IntegerField(write_only=True, required=False) + height = serializers.IntegerField(write_only=True, required=False) + margin = serializers.CharField(write_only=True, allow_null=True, + required=False, + default='center') + quality = serializers.IntegerField(write_only=True, allow_null=True, required=False, + default=settings.THUMBNAIL_QUALITY, + validators=[ + MinValueValidator(1), + MaxValueValidator(100)]) # RESPONSE url = serializers.ImageField(source='image', read_only=True) + cropped_image = serializers.DictField(read_only=True, allow_null=True) orientation_display = serializers.CharField(source='get_orientation_display', read_only=True) @@ -25,7 +40,46 @@ class ImageSerializer(serializers.ModelSerializer): 'orientation', 'orientation_display', 'title', + 'width', + 'height', + 'margin', + 'quality', + 'cropped_image', ] extra_kwargs = { 'orientation': {'write_only': True} } + + def validate(self, attrs): + """Overridden validate method.""" + image = attrs.get('image').image + crop_width = attrs.get('width') + crop_height = attrs.get('height') + margin = attrs.get('margin') + + if crop_height and crop_width and margin: + xy_image = (image.width, image.width) + xy_window = (crop_width, crop_height) + try: + parse_crop(margin, xy_image, xy_window) + except ThumbnailParseError: + raise serializers.ValidationError({'margin': 'Unrecognized crop option: %s' % margin}) + return attrs + + def create(self, validated_data): + """Overridden create method.""" + width = validated_data.pop('width', None) + height = validated_data.pop('height', None) + quality = validated_data.pop('quality') + margin = validated_data.pop('margin') + + instance = super().create(validated_data) + + if instance and width and height: + setattr(instance, + 'cropped_image', + instance.get_cropped_image( + geometry=f'{width}x{height}', + quality=quality, + margin=margin)) + return instance diff --git a/apps/utils/models.py b/apps/utils/models.py index 8c186f2f..59ec2282 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -227,6 +227,18 @@ class SORLImageMixin(models.Model): else: return None + def get_cropped_image(self, geometry: str, quality: int, margin: str) -> dict: + cropped_image = get_thumbnail(self.image, + geometry_string=geometry, + crop=margin, + quality=quality) + return { + 'geometry_string': geometry, + 'crop_url': cropped_image.url, + 'quality': quality, + 'margin': margin + } + image_tag.short_description = _('Image') image_tag.allow_tags = True diff --git a/project/settings/base.py b/project/settings/base.py index 2e29c92d..ef875c72 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -399,6 +399,13 @@ SORL_THUMBNAIL_ALIASES = { 'establishment_xlarge': {'geometry_string': '640x360', 'crop': 'center'}, 'establishment_detail': {'geometry_string': '2048x1152', 'crop': 'center'}, 'establishment_original': {'geometry_string': '1920x1080', 'crop': 'center'}, + 'city_xsmall': {'geometry_string': '70x70', 'crop': 'center'}, + 'city_small': {'geometry_string': '140x140', 'crop': 'center'}, + 'city_medium': {'geometry_string': '280x280', 'crop': 'center'}, + 'city_large': {'geometry_string': '280x280', 'crop': 'center'}, + 'city_xlarge': {'geometry_string': '560x560', 'crop': 'center'}, + 'city_detail': {'geometry_string': '1120x1120', 'crop': 'center'}, + 'city_original': {'geometry_string': '2048x1536', 'crop': 'center'}, } From 2ebe6e60a3b873b016871ea47c5fe60951c20ded Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 13:54:07 +0300 Subject: [PATCH 04/26] Facet with tags id instead of text values --- apps/search_indexes/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index b5bda947..d6a2938a 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -31,7 +31,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet): faceted_search_fields = { 'tag': { - 'field': 'tags.value', + 'field': 'tags.id', 'enabled': True, 'facet': TermsFacet, }, @@ -122,7 +122,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'enabled': True, }, 'tag': { - 'field': 'tags.value', + 'field': 'tags.id', 'facet': TermsFacet, 'enabled': True, }, From 17931eba5303164618c3907164b32d5e7e4ca68e Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 13:54:07 +0300 Subject: [PATCH 05/26] Facet with tags id instead of text values (cherry picked from commit 2ebe6e6) --- apps/search_indexes/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index b5bda947..d6a2938a 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -31,7 +31,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet): faceted_search_fields = { 'tag': { - 'field': 'tags.value', + 'field': 'tags.id', 'enabled': True, 'facet': TermsFacet, }, @@ -122,7 +122,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'enabled': True, }, 'tag': { - 'field': 'tags.value', + 'field': 'tags.id', 'facet': TermsFacet, 'enabled': True, }, From 59f44d84f1b311f87b99df25bd0d25ece367554f Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 14:27:40 +0300 Subject: [PATCH 06/26] Establishment facet visibble tags only --- apps/search_indexes/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index d6a2938a..e37d19b3 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -122,7 +122,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'enabled': True, }, 'tag': { - 'field': 'tags.id', + 'field': 'visible_tags.id', 'facet': TermsFacet, 'enabled': True, }, From 0231f9770994bed8dab33972c28281e8e70b6f19 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 14:27:40 +0300 Subject: [PATCH 07/26] Establishment facet visibble tags only (cherry picked from commit 59f44d8) --- apps/search_indexes/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index d6a2938a..e37d19b3 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -122,7 +122,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'enabled': True, }, 'tag': { - 'field': 'tags.id', + 'field': 'visible_tags.id', 'facet': TermsFacet, 'enabled': True, }, From 120e686be65364db77a86b48933e588de60a7a09 Mon Sep 17 00:00:00 2001 From: Semyon Yekhmenin Date: Fri, 22 Nov 2019 11:46:24 +0000 Subject: [PATCH 08/26] Changed in_favorites field to read only --- 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 2264b9ec..c96d05da 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -65,7 +65,7 @@ class NewsBaseSerializer(ProjectModelSerializer): subtitle_translated = TranslatedField() news_type = NewsTypeSerializer(read_only=True) tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags') - in_favorites = serializers.BooleanField(allow_null=True) + in_favorites = serializers.BooleanField(allow_null=True, read_only=True) view_counter = serializers.IntegerField(read_only=True) class Meta: From 844c8525e9d73d3386b918d88de6b65d1f07557c Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 22 Nov 2019 14:46:31 +0300 Subject: [PATCH 09/26] refactored, add endpoint - /api/back/gallery//crop/ --- apps/gallery/serializers.py | 78 ++++++++++++++++++++++++++----------- apps/gallery/urls.py | 5 ++- apps/gallery/views.py | 5 +++ 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index 1f96dca8..2c2e50d0 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -3,6 +3,7 @@ from django.core.validators import MinValueValidator, MaxValueValidator from rest_framework import serializers from sorl.thumbnail.parsers import parse_crop from sorl.thumbnail.parsers import ThumbnailParseError +from django.utils.translation import gettext_lazy as _ from . import models @@ -12,21 +13,10 @@ class ImageSerializer(serializers.ModelSerializer): # REQUEST file = serializers.ImageField(source='image', write_only=True) - width = serializers.IntegerField(write_only=True, required=False) - height = serializers.IntegerField(write_only=True, required=False) - margin = serializers.CharField(write_only=True, allow_null=True, - required=False, - default='center') - quality = serializers.IntegerField(write_only=True, allow_null=True, required=False, - default=settings.THUMBNAIL_QUALITY, - validators=[ - MinValueValidator(1), - MaxValueValidator(100)]) # RESPONSE url = serializers.ImageField(source='image', read_only=True) - cropped_image = serializers.DictField(read_only=True, allow_null=True) orientation_display = serializers.CharField(source='get_orientation_display', read_only=True) @@ -40,30 +30,55 @@ class ImageSerializer(serializers.ModelSerializer): 'orientation', 'orientation_display', 'title', + ] + extra_kwargs = { + 'orientation': {'write_only': True} + } + + +class CropImageSerializer(ImageSerializer): + """Serializers for image crops.""" + + width = serializers.IntegerField(write_only=True) + height = serializers.IntegerField(write_only=True) + margin = serializers.CharField(write_only=True, allow_null=True, + required=False, + default='center') + quality = serializers.IntegerField(write_only=True, allow_null=True, required=False, + default=settings.THUMBNAIL_QUALITY, + validators=[ + MinValueValidator(1), + MaxValueValidator(100)]) + cropped_image = serializers.DictField(read_only=True, allow_null=True) + + class Meta(ImageSerializer.Meta): + """Meta class.""" + fields = [ + 'id', + 'url', + 'orientation_display', 'width', 'height', 'margin', 'quality', 'cropped_image', ] - extra_kwargs = { - 'orientation': {'write_only': True} - } def validate(self, attrs): """Overridden validate method.""" - image = attrs.get('image').image + file = self._image.image crop_width = attrs.get('width') crop_height = attrs.get('height') margin = attrs.get('margin') if crop_height and crop_width and margin: - xy_image = (image.width, image.width) + xy_image = (file.width, file.width) xy_window = (crop_width, crop_height) try: parse_crop(margin, xy_image, xy_window) + attrs['image'] = file except ThumbnailParseError: - raise serializers.ValidationError({'margin': 'Unrecognized crop option: %s' % margin}) + raise serializers.ValidationError({'margin': _('Unrecognized crop option: %s') % margin}) return attrs def create(self, validated_data): @@ -73,13 +88,32 @@ class ImageSerializer(serializers.ModelSerializer): quality = validated_data.pop('quality') margin = validated_data.pop('margin') - instance = super().create(validated_data) + image = self._image - if instance and width and height: - setattr(instance, + if image and width and height: + setattr(image, 'cropped_image', - instance.get_cropped_image( + image.get_cropped_image( geometry=f'{width}x{height}', quality=quality, margin=margin)) - return instance + return image + + @property + def view(self): + return self.context.get('view') + + @property + def lookup_field(self): + lookup_field = 'pk' + + if lookup_field in self.view.kwargs: + return self.view.kwargs.get(lookup_field) + + @property + def _image(self): + """Return image from url_kwargs.""" + qs = models.Image.objects.filter(id=self.lookup_field) + if qs.exists(): + return qs.first() + raise serializers.ValidationError({'detail': _('Image not found.')}) diff --git a/apps/gallery/urls.py b/apps/gallery/urls.py index 8258092c..987685cb 100644 --- a/apps/gallery/urls.py +++ b/apps/gallery/urls.py @@ -6,6 +6,7 @@ from . import views app_name = 'gallery' urlpatterns = [ - path('', views.ImageListCreateView.as_view(), name='list-create-image'), - path('/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy-image'), + path('', views.ImageListCreateView.as_view(), name='list-create'), + path('/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy'), + path('/crop/', views.CropImageCreateView.as_view(), name='create-crop'), ] diff --git a/apps/gallery/views.py b/apps/gallery/views.py index 2b155035..1515707f 100644 --- a/apps/gallery/views.py +++ b/apps/gallery/views.py @@ -28,3 +28,8 @@ class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView): else: on_commit(lambda: tasks.delete_image(image_id=instance.id)) return Response(status=status.HTTP_204_NO_CONTENT) + + +class CropImageCreateView(ImageBaseView, generics.CreateAPIView): + """Create crop image.""" + serializer_class = serializers.CropImageSerializer From 429825804a60793e3e7ebbca332bcafc01a60b34 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 22 Nov 2019 15:07:50 +0300 Subject: [PATCH 10/26] create destroy mixin --- apps/news/models.py | 1 + apps/news/urls/back.py | 2 +- apps/utils/views.py | 45 +++++++++++++++++++++++++++++------------- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/apps/news/models.py b/apps/news/models.py index 8a86e688..0d510804 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -196,6 +196,7 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi views_count = models.OneToOneField('rating.ViewCount', blank=True, null=True, on_delete=models.SET_NULL) ratings = generic.GenericRelation(Rating) favorites = generic.GenericRelation(to='favorites.Favorites') + carousels = generic.GenericRelation(to='main.Carousel') agenda = models.ForeignKey('news.Agenda', blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('agenda')) diff --git a/apps/news/urls/back.py b/apps/news/urls/back.py index 9cc3d94a..7e54928f 100644 --- a/apps/news/urls/back.py +++ b/apps/news/urls/back.py @@ -13,4 +13,4 @@ urlpatterns = [ name='gallery-list'), path('/gallery//', views.NewsBackOfficeGalleryCreateDestroyView.as_view(), name='gallery-create-destroy'), -] \ No newline at end of file +] diff --git a/apps/utils/views.py b/apps/utils/views.py index fef14c08..e08e0bf5 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -85,7 +85,7 @@ class JWTGenericViewMixin: value=cookie.value, secure=cookie.secure, httponly=cookie.http_only, - max_age=cookie.max_age,) + max_age=cookie.max_age, ) return response def _get_tokens_from_cookies(self, request, cookies: dict = None): @@ -126,9 +126,8 @@ class CreateDestroyGalleryViewMixin(generics.CreateAPIView, return Response(status=status.HTTP_204_NO_CONTENT) -class FavoritesCreateDestroyMixinView(generics.CreateAPIView, - generics.DestroyAPIView): - """Favorites Create Destroy mixin.""" +class BaseCreateDestroyMixinView(generics.CreateAPIView, generics.DestroyAPIView): + """Base Create Destroy mixin.""" _model = None serializer_class = None @@ -137,16 +136,6 @@ class FavoritesCreateDestroyMixinView(generics.CreateAPIView, def get_base_object(self): return get_object_or_404(self._model, slug=self.kwargs['slug']) - def get_object(self): - """ - Returns the object the view is displaying. - """ - obj = self.get_base_object() - favorites = get_object_or_404(obj.favorites.filter(user=self.request.user)) - # May raise a permission denied - self.check_object_permissions(self.request, favorites) - return favorites - def es_update_base_object(self): es_update(self.get_base_object()) @@ -159,6 +148,34 @@ class FavoritesCreateDestroyMixinView(generics.CreateAPIView, self.es_update_base_object() +class FavoritesCreateDestroyMixinView(BaseCreateDestroyMixinView): + """Favorites Create Destroy mixin.""" + + def get_object(self): + """ + Returns the object the view is displaying. + """ + obj = self.get_base_object() + favorites = get_object_or_404(obj.favorites.filter(user=self.request.user)) + # May raise a permission denied + self.check_object_permissions(self.request, favorites) + return favorites + + +class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView): + """Carousel Create Destroy mixin.""" + + def get_object(self): + """ + Returns the object the view is displaying. + """ + obj = self.get_base_object() + carousels = get_object_or_404(obj.carousels.filter(user=self.request.user)) + # May raise a permission denied + self.check_object_permissions(self.request, carousels) + return carousels + + # BackOffice user`s views & viewsets class BindObjectMixin: """Bind object mixin.""" From f95a082db395b2668671c49e74d28542c4995890 Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Fri, 22 Nov 2019 16:00:06 +0300 Subject: [PATCH 11/26] update geoip feature --- apps/establishment/views/web.py | 3 +- apps/main/methods.py | 52 +++++++++++++-------------------- apps/main/views/common.py | 5 ++-- apps/main/views/web.py | 3 +- load_geiopdb.sh | 23 --------------- project/settings/base.py | 1 - 6 files changed, 25 insertions(+), 62 deletions(-) delete mode 100755 load_geiopdb.sh diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 4f1fe07c..0b6f1ba0 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -56,12 +56,11 @@ class EstablishmentRecentReviewListView(EstablishmentListView): def get_queryset(self): """Overridden method 'get_queryset'.""" qs = super().get_queryset() - user_ip = methods.get_user_ip(self.request) query_params = self.request.query_params if 'longitude' in query_params and 'latitude' in query_params: longitude, latitude = query_params.get('longitude'), query_params.get('latitude') else: - longitude, latitude = methods.determine_coordinates(user_ip) + longitude, latitude = methods.determine_coordinates(self.request) if not longitude or not latitude: return qs.none() point = Point(x=float(longitude), y=float(latitude), srid=settings.GEO_DEFAULT_SRID) diff --git a/apps/main/methods.py b/apps/main/methods.py index d5f307eb..f19d595a 100644 --- a/apps/main/methods.py +++ b/apps/main/methods.py @@ -28,31 +28,25 @@ def get_user_ip(request): return ip -def determine_country_code(ip_addr): +def determine_country_code(request): """Determine country code.""" - country_code = None - if ip_addr: - try: - geoip = GeoIP2() - country_code = geoip.country_code(ip_addr) - country_code = country_code.lower() - except GeoIP2Exception as ex: - logger.info(f'GEOIP Exception: {ex}. ip: {ip_addr}') - except Exception as ex: - logger.error(f'GEOIP Base exception: {ex}') - return country_code + META = request.META + country_code = META.get('X-GeoIP-Country-Code', + META.get('HTTP_X_GEOIP_COUNTRY_CODE')) + if isinstance(country_code, str): + return country_code.lower() -def determine_coordinates(ip_addr: str) -> Tuple[Optional[float], Optional[float]]: - if ip_addr: - try: - geoip = GeoIP2() - return geoip.coords(ip_addr) - except GeoIP2Exception as ex: - logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}') - except Exception as ex: - logger.warning(f'GEOIP Base exception: {ex}') - return None, None +def determine_coordinates(request): + META = request.META + longitude = META.get('X-GeoIP-Longitude', + META.get('HTTP_X_GEOIP_LONGITUDE')) + latitude = META.get('X-GeoIP-Latitude', + META.get('HTTP_X_GEOIP_LATITUDE')) + try: + return float(longitude), float(latitude) + except (TypeError, ValueError): + return None, None def determine_user_site_url(country_code): @@ -76,15 +70,11 @@ def determine_user_site_url(country_code): return site.site_url -def determine_user_city(ip_addr: str) -> Optional[City]: - try: - geoip = GeoIP2() - return geoip.city(ip_addr) - except GeoIP2Exception as ex: - logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}') - except Exception as ex: - logger.warning(f'GEOIP Base exception: {ex}') - return None +def determine_user_city(request): + META = request.META + city = META.get('X-GeoIP-City', + META.get('HTTP_X_GEOIP_CITY')) + return city def determine_subdivision( diff --git a/apps/main/views/common.py b/apps/main/views/common.py index 18ee0d8d..674d045e 100644 --- a/apps/main/views/common.py +++ b/apps/main/views/common.py @@ -86,9 +86,8 @@ class DetermineLocation(generics.GenericAPIView): serializer_class = EmptySerializer def get(self, request, *args, **kwargs): - user_ip = methods.get_user_ip(request) - longitude, latitude = methods.determine_coordinates(user_ip) - city = methods.determine_user_city(user_ip) + longitude, latitude = methods.determine_coordinates(request) + city = methods.determine_user_city(request) if longitude and latitude and city: return Response(data={'latitude': latitude, 'longitude': longitude, 'city': city}) else: diff --git a/apps/main/views/web.py b/apps/main/views/web.py index e1dc32ef..3a634457 100644 --- a/apps/main/views/web.py +++ b/apps/main/views/web.py @@ -14,8 +14,7 @@ class DetermineSiteView(generics.GenericAPIView): serializer_class = EmptySerializer def get(self, request, *args, **kwargs): - user_ip = methods.get_user_ip(request) - country_code = methods.determine_country_code(user_ip) + country_code = methods.determine_country_code(request) url = methods.determine_user_site_url(country_code) return Response(data={'url': url}) diff --git a/load_geiopdb.sh b/load_geiopdb.sh deleted file mode 100755 index 48d16af1..00000000 --- a/load_geiopdb.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -DB_CITY_URL="https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz" -DB_COUNTRY_URL="https://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz" -DIR_PATH="geoip_db" -ARCH_PATH="archive" - -mkdir -p $DIR_PATH -cd $DIR_PATH - -mkdir -p $ARCH_PATH - -find . -not -path "./$ARCH_PATH/*" -type f -name "*.mmdb" -exec mv -t "./$ARCH_PATH/" {} \+ - -filename=$(basename $DB_CITY_URL) -wget -O $filename $DB_CITY_URL -tar xzvf "$filename" - -filename=$(basename $DB_COUNTRY_URL) -wget -O $filename $DB_COUNTRY_URL -tar xzvf "$filename" - -find . -mindepth 1 -type f -name "*.mmdb" -not -path "./$ARCH_PATH/*" -exec mv -t . {} \+ diff --git a/project/settings/base.py b/project/settings/base.py index 2e29c92d..87f85065 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -487,7 +487,6 @@ LIMITING_QUERY_OBJECTS = QUERY_OUTPUT_OBJECTS * 3 # GEO # A Spatial Reference System Identifier GEO_DEFAULT_SRID = 4326 -GEOIP_PATH = os.path.join(PROJECT_ROOT, 'geoip_db') # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ From 7e878956e37c01b807bc7f916462a6a94e619fa3 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 22 Nov 2019 16:22:31 +0300 Subject: [PATCH 12/26] carousel api for news --- apps/news/serializers.py | 23 ++++++++++++++++++++++- apps/news/urls/common.py | 5 ++++- apps/news/views.py | 9 ++++++++- apps/utils/exceptions.py | 8 ++++++++ apps/utils/serializers.py | 26 ++++++++++++++++++++++---- apps/utils/views.py | 5 +++-- 6 files changed, 67 insertions(+), 9 deletions(-) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index c96d05da..f58e7c24 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -11,7 +11,7 @@ from news import models from tag.serializers import TagBaseSerializer from utils import exceptions as utils_exceptions from utils.serializers import (TranslatedField, ProjectModelSerializer, - FavoritesCreateSerializer, ImageBaseSerializer) + FavoritesCreateSerializer, ImageBaseSerializer, CarouselCreateSerializer) class AgendaSerializer(ProjectModelSerializer): @@ -269,3 +269,24 @@ class NewsFavoritesCreateSerializer(FavoritesCreateSerializer): 'content_object': validated_data.pop('news') }) return super().create(validated_data) + + +class NewsCarouselCreateSerializer(CarouselCreateSerializer): + """Serializer to carousel object w/ model News.""" + + def validate(self, attrs): + news = models.News.objects.filter(slug=self.slug).first() + if not news: + raise serializers.ValidationError({'detail': _('Object not found.')}) + + if news.carousels.exists(): + raise utils_exceptions.CarouselError() + + attrs['news'] = news + return attrs + + def create(self, validated_data, *args, **kwargs): + validated_data.update({ + 'content_object': validated_data.pop('news') + }) + return super().create(validated_data) diff --git a/apps/news/urls/common.py b/apps/news/urls/common.py index b42905eb..e2aae7a1 100644 --- a/apps/news/urls/common.py +++ b/apps/news/urls/common.py @@ -5,5 +5,8 @@ common_urlpatterns = [ path('', views.NewsListView.as_view(), name='list'), path('types/', views.NewsTypeListView.as_view(), name='type'), path('slug//', views.NewsDetailView.as_view(), name='rud'), - path('slug//favorites/', views.NewsFavoritesCreateDestroyView.as_view(), name='create-destroy-favorites') + path('slug//favorites/', views.NewsFavoritesCreateDestroyView.as_view(), + name='create-destroy-favorites'), + path('slug//carousels/', views.NewsCarouselCreateDestroyView.as_view(), + name='create-destroy-carousels'), ] diff --git a/apps/news/views.py b/apps/news/views.py index bdb75fc7..7845ab0d 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -6,7 +6,7 @@ from rest_framework import generics, permissions from news import filters, models, serializers from rating.tasks import add_rating from utils.permissions import IsCountryAdmin, IsContentPageManager -from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView +from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView from utils.serializers import ImageBaseSerializer @@ -155,3 +155,10 @@ class NewsFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView): _model = models.News serializer_class = serializers.NewsFavoritesCreateSerializer + + +class NewsCarouselCreateDestroyView(CarouselCreateDestroyMixinView): + """View for create/destroy news from carousel.""" + + _model = models.News + serializer_class = serializers.NewsCarouselCreateSerializer diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index 37786ce7..c82ff023 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -135,6 +135,14 @@ class FavoritesError(exceptions.APIException): default_detail = _('Item is already in favorites.') +class CarouselError(exceptions.APIException): + """ + The exception should be thrown when the object is already in carousels. + """ + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _('Item is already in carousels.') + + class PasswordResetRequestExistedError(exceptions.APIException): """ The exception should be thrown when password reset request diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index b78c202c..634246b7 100644 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -2,10 +2,11 @@ import pytz from django.core import exceptions from rest_framework import serializers -from utils import models -from translation.models import Language + from favorites.models import Favorites -from gallery.models import Image +from main.models import Carousel +from translation.models import Language +from utils import models class EmptySerializer(serializers.Serializer): @@ -80,7 +81,6 @@ class FavoritesCreateSerializer(serializers.ModelSerializer): """Serializer to favorite object.""" class Meta: - """Serializer for model Comment.""" model = Favorites fields = [ 'id', @@ -101,6 +101,24 @@ class FavoritesCreateSerializer(serializers.ModelSerializer): return self.request.parser_context.get('kwargs').get('slug') +class CarouselCreateSerializer(serializers.ModelSerializer): + """Carousel to favorite object.""" + + class Meta: + model = Carousel + fields = [ + 'id', + ] + + @property + def request(self): + return self.context.get('request') + + @property + def slug(self): + return self.request.parser_context.get('kwargs').get('slug') + + class RecursiveFieldSerializer(serializers.Serializer): def to_representation(self, value): serializer = self.parent.parent.__class__(value, context=self.context) diff --git a/apps/utils/views.py b/apps/utils/views.py index e08e0bf5..c09df2a2 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -170,9 +170,10 @@ class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView): Returns the object the view is displaying. """ obj = self.get_base_object() - carousels = get_object_or_404(obj.carousels.filter(user=self.request.user)) + carousels = get_object_or_404(obj.carousels.all()) # May raise a permission denied - self.check_object_permissions(self.request, carousels) + # TODO: возможно нужны пермишены + # self.check_object_permissions(self.request, carousels) return carousels From fa183bfc92b343565384b0857dc33fdd11b13c4f Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 22 Nov 2019 17:00:43 +0300 Subject: [PATCH 13/26] carousel back api url --- apps/news/urls/back.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/news/urls/back.py b/apps/news/urls/back.py index 7e54928f..9126b3e9 100644 --- a/apps/news/urls/back.py +++ b/apps/news/urls/back.py @@ -13,4 +13,6 @@ urlpatterns = [ name='gallery-list'), path('/gallery//', views.NewsBackOfficeGalleryCreateDestroyView.as_view(), name='gallery-create-destroy'), + path('slug//carousels/', views.NewsCarouselCreateDestroyView.as_view(), + name='create-destroy-carousels'), ] From a5d5dc133546e6f5862eb8dff0e400fd382e6165 Mon Sep 17 00:00:00 2001 From: alex Date: Fri, 22 Nov 2019 17:13:41 +0300 Subject: [PATCH 14/26] establishment carousel web and mobile api --- apps/establishment/models.py | 1 + apps/establishment/serializers/common.py | 23 ++++++++++++++++++++++- apps/establishment/urls/common.py | 4 +++- apps/establishment/views/web.py | 17 ++++++++++++----- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index f0260cf9..f8ffd456 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -395,6 +395,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, verbose_name=_('Tag')) reviews = generic.GenericRelation(to='review.Review') comments = generic.GenericRelation(to='comment.Comment') + carousels = generic.GenericRelation(to='main.Carousel') favorites = generic.GenericRelation(to='favorites.Favorites') currency = models.ForeignKey(Currency, blank=True, null=True, default=None, on_delete=models.PROTECT, diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 0c183477..c85b3123 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -13,7 +13,7 @@ from review.serializers import ReviewShortSerializer from tag.serializers import TagBaseSerializer from timetable.serialziers import ScheduleRUDSerializer from utils import exceptions as utils_exceptions -from utils.serializers import ImageBaseSerializer +from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer from utils.serializers import (ProjectModelSerializer, TranslatedField, FavoritesCreateSerializer) @@ -426,6 +426,27 @@ class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer): return super().create(validated_data) +class EstablishmentCarouselCreateSerializer(CarouselCreateSerializer): + """Serializer to carousel object w/ model News.""" + + def validate(self, attrs): + establishment = models.Establishment.objects.filter(slug=self.slug).first() + if not establishment: + raise serializers.ValidationError({'detail': _('Object not found.')}) + + if establishment.carousels.exists(): + raise utils_exceptions.CarouselError() + + attrs['establishment'] = establishment + return attrs + + def create(self, validated_data, *args, **kwargs): + validated_data.update({ + 'content_object': validated_data.pop('establishment') + }) + return super().create(validated_data) + + class CompanyBaseSerializer(serializers.ModelSerializer): """Company base serializer""" phone_list = serializers.SerializerMethodField(source='phones', read_only=True) diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 49cd3631..3f46df90 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -17,5 +17,7 @@ urlpatterns = [ path('slug//comments//', views.EstablishmentCommentRUDView.as_view(), name='rud-comment'), path('slug//favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), - name='create-destroy-favorites') + name='create-destroy-favorites'), + path('slug//carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(), + name='create-destroy-carousels') ] diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 0b6f1ba0..bd826e4e 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -9,7 +9,7 @@ from comment.serializers import CommentRUDSerializer from establishment import filters, models, serializers from main import methods from utils.pagination import EstablishmentPortionPagination -from utils.views import FavoritesCreateDestroyMixinView +from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView class EstablishmentMixinView: @@ -34,7 +34,7 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView): serializer_class = serializers.EstablishmentListRetrieveSerializer def get_queryset(self): - return super().get_queryset().with_schedule()\ + return super().get_queryset().with_schedule() \ .with_extended_address_related().with_currency_related() @@ -105,9 +105,9 @@ class EstablishmentCommentListView(generics.ListAPIView): establishment = get_object_or_404(models.Establishment, slug=self.kwargs['slug']) return comment_models.Comment.objects.by_content_type(app_label='establishment', - model='establishment')\ - .by_object_id(object_id=establishment.pk)\ - .order_by('-created') + model='establishment') \ + .by_object_id(object_id=establishment.pk) \ + .order_by('-created') class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView): @@ -139,6 +139,13 @@ class EstablishmentFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView): serializer_class = serializers.EstablishmentFavoritesCreateSerializer +class EstablishmentCarouselCreateDestroyView(CarouselCreateDestroyMixinView): + """View for create/destroy establishment from carousel.""" + + _model = models.Establishment + serializer_class = serializers.EstablishmentCarouselCreateSerializer + + class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView): """Resource for getting list of nearest establishments.""" From e565406729509fa9f3028c63ece228cdfbe0b985 Mon Sep 17 00:00:00 2001 From: Semyon Yekhmenin Date: Fri, 22 Nov 2019 14:21:25 +0000 Subject: [PATCH 15/26] Feature/bo employee update --- apps/establishment/filters.py | 20 +++++++ .../migrations/0066_auto_20191122_1144.py | 28 +++++++++ .../migrations/0067_auto_20191122_1244.py | 39 ++++++++++++ apps/establishment/models.py | 59 +++++++++++++++++-- apps/establishment/serializers/back.py | 46 ++++++++++++++- apps/establishment/serializers/common.py | 57 +++++++++++++++++- apps/establishment/urls/back.py | 8 +++ apps/establishment/views/back.py | 44 +++++++++++++- 8 files changed, 289 insertions(+), 12 deletions(-) create mode 100644 apps/establishment/migrations/0066_auto_20191122_1144.py create mode 100644 apps/establishment/migrations/0067_auto_20191122_1244.py diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index adbcae76..db419989 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -55,3 +55,23 @@ class EstablishmentTypeTagFilter(filters.FilterSet): fields = ( 'type_id', ) + + +class EmployeeBackFilter(filters.FilterSet): + """Employee filter set.""" + + search = filters.CharFilter(method='search_by_name_or_last_name') + + class Meta: + """Meta class.""" + + model = models.Employee + fields = ( + 'search', + ) + + 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 diff --git a/apps/establishment/migrations/0066_auto_20191122_1144.py b/apps/establishment/migrations/0066_auto_20191122_1144.py new file mode 100644 index 00000000..edff3333 --- /dev/null +++ b/apps/establishment/migrations/0066_auto_20191122_1144.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.7 on 2019-11-22 11:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0065_establishment_purchased_products'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='last_name', + field=models.CharField(default=None, max_length=255, null=True, verbose_name='Last Name'), + ), + migrations.AddField( + model_name='establishmentemployee', + name='status', + field=models.CharField(choices=[('I', 'Idle'), ('A', 'Accepted'), ('D', 'Declined')], default='I', max_length=1), + ), + migrations.AlterField( + model_name='employee', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + ] diff --git a/apps/establishment/migrations/0067_auto_20191122_1244.py b/apps/establishment/migrations/0067_auto_20191122_1244.py new file mode 100644 index 00000000..8dfdc3a4 --- /dev/null +++ b/apps/establishment/migrations/0067_auto_20191122_1244.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2.7 on 2019-11-22 12:44 + +from django.db import migrations, models +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0066_auto_20191122_1144'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='birth_date', + field=models.DateTimeField(default=None, null=True, verbose_name='Birth date'), + ), + migrations.AddField( + model_name='employee', + name='email', + field=models.EmailField(blank=True, default=None, max_length=254, null=True, verbose_name='Email'), + ), + migrations.AddField( + model_name='employee', + name='phone', + field=phonenumber_field.modelfields.PhoneNumberField(default=None, max_length=128, null=True), + ), + migrations.AddField( + model_name='employee', + name='sex', + field=models.PositiveSmallIntegerField(choices=[(0, 'Male'), (1, 'Female')], default=None, null=True, verbose_name='Sex'), + ), + migrations.AddField( + model_name='employee', + name='toque_number', + field=models.PositiveSmallIntegerField(default=None, null=True, verbose_name='Toque number'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index f8ffd456..d22aebaa 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1,6 +1,7 @@ """Establishment models.""" from datetime import datetime from functools import reduce +from typing import List from operator import or_ import elasticsearch_dsl @@ -435,11 +436,12 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, @property def visible_tags(self): - return super().visible_tags\ - .exclude(category__index_name__in=['guide', 'collection', 'purchased_item', - 'business_tag', 'business_tags_de'])\ + return super().visible_tags \ + .exclude(category__index_name__in=['guide', 'collection', 'purchased_item', + 'business_tag', 'business_tags_de']) \ + \ + # todo: recalculate toque_number - # todo: recalculate toque_number def recalculate_toque_number(self): toque_number = 0 if self.address and self.public_mark: @@ -612,7 +614,6 @@ class EstablishmentNote(ProjectBaseMixin): class EstablishmentGallery(IntermediateGalleryModelMixin): - establishment = models.ForeignKey(Establishment, null=True, related_name='establishment_gallery', on_delete=models.CASCADE, @@ -663,6 +664,16 @@ class EstablishmentEmployeeQuerySet(models.QuerySet): class EstablishmentEmployee(BaseAttributes): """EstablishmentEmployee model.""" + IDLE = 'I' + ACCEPTED = 'A' + DECLINED = 'D' + + STATUS_CHOICES = ( + (IDLE, 'Idle'), + (ACCEPTED, 'Accepted'), + (DECLINED, 'Declined'), + ) + establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT, verbose_name=_('Establishment')) employee = models.ForeignKey('establishment.Employee', on_delete=models.PROTECT, @@ -673,19 +684,53 @@ class EstablishmentEmployee(BaseAttributes): verbose_name=_('To date')) position = models.ForeignKey(Position, on_delete=models.PROTECT, verbose_name=_('Position')) + + status = models.CharField(max_length=1, choices=STATUS_CHOICES, default=IDLE) + # old_id = affiliations_id old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True) objects = EstablishmentEmployeeQuerySet.as_manager() +class EmployeeQuerySet(models.QuerySet): + + def _generic_search(self, value, filter_fields_names: List[str]): + """Generic method for searching value in specified fields""" + filters = [ + {f'{field}__icontains': value} + for field in filter_fields_names + ] + return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters])) + + def search_by_name_or_last_name(self, value): + """Search by name or last_name.""" + return self._generic_search(value, ['name', 'last_name']) + + class Employee(BaseAttributes): """Employee model.""" user = models.OneToOneField('account.User', on_delete=models.PROTECT, null=True, blank=True, default=None, verbose_name=_('User')) - name = models.CharField(max_length=255, verbose_name=_('Last name')) + name = models.CharField(max_length=255, verbose_name=_('Name')) + last_name = models.CharField(max_length=255, verbose_name=_('Last Name'), null=True, default=None) + + # SEX CHOICES + MALE = 0 + FEMALE = 1 + + SEX_CHOICES = ( + (MALE, _('Male')), + (FEMALE, _('Female')) + ) + sex = models.PositiveSmallIntegerField(choices=SEX_CHOICES, verbose_name=_('Sex'), null=True, default=None) + birth_date = models.DateTimeField(editable=True, verbose_name=_('Birth date'), null=True, default=None) + email = models.EmailField(blank=True, null=True, default=None, verbose_name=_('Email')) + phone = PhoneNumberField(null=True, default=None) + toque_number = models.PositiveSmallIntegerField(verbose_name=_('Toque number'), null=True, default=None) + establishments = models.ManyToManyField(Establishment, related_name='employees', through=EstablishmentEmployee, ) awards = generic.GenericRelation(to='main.Award', related_query_name='employees') @@ -694,6 +739,8 @@ class Employee(BaseAttributes): # old_id = profile_id old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True) + objects = EmployeeQuerySet.as_manager() + class Meta: """Meta class.""" diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index a78bce07..52af0574 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -2,8 +2,9 @@ from rest_framework import serializers from establishment import models from establishment import serializers as model_serializers -from location.serializers import AddressDetailSerializer +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 gallery.models import Image @@ -161,12 +162,53 @@ class ContactEmailBackSerializers(model_serializers.PlateSerializer): class EmployeeBackSerializers(serializers.ModelSerializer): """Employee serializers.""" + awards = AwardSerializer(many=True) + class Meta: model = models.Employee fields = [ 'id', 'user', - 'name' + 'name', + 'last_name', + 'sex', + 'birth_date', + 'email', + 'phone', + 'toque_number', + 'awards', + ] + + +class PositionBackSerializer(serializers.ModelSerializer): + """Position Back serializer.""" + + name_translated = TranslatedField() + + class Meta: + model = models.Position + fields = [ + 'id', + 'name_translated', + 'priority', + 'index_name', + ] + + +class EstablishmentEmployeeBackSerializer(serializers.ModelSerializer): + """Establishment Employee serializer.""" + + employee = EmployeeBackSerializers() + position = PositionBackSerializer() + + class Meta: + model = models.EstablishmentEmployee + fields = [ + 'id', + 'employee', + 'from_date', + 'to_date', + 'position', ] diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index c85b3123..9419c449 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -168,12 +168,51 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer): awards = AwardSerializer(source='employee.awards', many=True) priority = serializers.IntegerField(source='position.priority') position_index_name = serializers.CharField(source='position.index_name') + status = serializers.CharField() class Meta: """Meta class.""" model = models.Employee - fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name') + fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name', 'status') + + +class EstablishmentEmployeeCreateSerializer(serializers.ModelSerializer): + """Serializer for establishment employee relation.""" + + class Meta: + """Meta class.""" + + model = models.EstablishmentEmployee + fields = ('id',) + + def _validate_entity(self, entity_id_param: str, entity_class): + entity_id = self.context.get('request').parser_context.get('kwargs').get(entity_id_param) + entity_qs = entity_class.objects.filter(id=entity_id) + if not entity_qs.exists(): + raise serializers.ValidationError({'detail': _(f'{entity_class.__name__} not found.')}) + return entity_qs.first() + + def validate(self, attrs): + """Override validate method""" + establishment = self._validate_entity("establishment_id", models.Establishment) + employee = self._validate_entity("employee_id", models.Employee) + position = self._validate_entity("position_id", models.Position) + + attrs['establishment'] = establishment + attrs['employee'] = employee + attrs['position'] = position + + return attrs + + def create(self, validated_data, *args, **kwargs): + """Override create method""" + validated_data.update({ + 'employee': validated_data.pop('employee'), + 'establishment': validated_data.pop('establishment'), + 'position': validated_data.pop("position") + }) + return super().create(validated_data) class EstablishmentShortSerializer(serializers.ModelSerializer): @@ -396,6 +435,22 @@ class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer return super().create(validated_data) +class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer): + """Retrieve/Update/Destroy comment serializer.""" + + class Meta: + """Meta class.""" + model = comment_models.Comment + fields = [ + 'id', + 'created', + 'text', + 'mark', + 'nickname', + 'profile_pic', + ] + + class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer): """Serializer to favorite object w/ model Establishment.""" diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index f06e2187..a06adc3b 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -38,8 +38,16 @@ urlpatterns = [ path('phones//', views.PhonesRUDView.as_view(), name='phones-rud'), path('emails/', views.EmailListCreateView.as_view(), name='emails'), path('emails//', views.EmailRUDView.as_view(), name='emails-rud'), + path('/employees/', views.EstablishmentEmployeeListView.as_view(), + name='establishment-employees'), path('employees/', views.EmployeeListCreateView.as_view(), name='employees'), path('employees//', views.EmployeeRUDView.as_view(), name='employees-rud'), + path('/employee//position/', + views.EstablishmentEmployeeCreateView.as_view(), + name='employees-establishment-create'), + path('/employee/', + views.EstablishmentEmployeeDeleteView.as_view(), + name='employees-establishment-delete'), path('types/', views.EstablishmentTypeListCreateView.as_view(), name='type-list'), path('types//', views.EstablishmentTypeRUDView.as_view(), name='type-rud'), path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'), diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index d1897397..312bb171 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -1,7 +1,9 @@ """Establishment app views.""" +from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404 -from rest_framework import generics, permissions +from rest_framework import generics, permissions, status +from utils.permissions import IsCountryAdmin, IsEstablishmentManager from establishment import filters, models, serializers from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer from utils.permissions import IsCountryAdmin, IsEstablishmentManager @@ -43,8 +45,8 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView): """ Returns the object the view is displaying. """ - establishment_pk = self.kwargs.get('pk') - schedule_id = self.kwargs.get('schedule_id') + establishment_pk = self.kwargs['pk'] + schedule_id = self.kwargs['schedule_id'] establishment = get_object_or_404(klass=models.Establishment.objects.all(), pk=establishment_pk) @@ -156,11 +158,23 @@ class EmailRUDView(generics.RetrieveUpdateDestroyAPIView): class EmployeeListCreateView(generics.ListCreateAPIView): """Emplyoee list create view.""" + permission_classes = (permissions.AllowAny, ) + filter_class = filters.EmployeeBackFilter serializer_class = serializers.EmployeeBackSerializers queryset = models.Employee.objects.all() pagination_class = None +class EstablishmentEmployeeListView(generics.ListAPIView): + """Establishment emplyoees list view.""" + permission_classes = (permissions.AllowAny, ) + serializer_class = serializers.EstablishmentEmployeeBackSerializer + + def get_queryset(self): + establishment_id = self.kwargs['establishment_id'] + return models.EstablishmentEmployee.objects.filter(establishment__id=establishment_id) + + class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView): """Employee RUD view.""" serializer_class = serializers.EmployeeBackSerializers @@ -318,3 +332,27 @@ class EstablishmentNoteRUDView(EstablishmentMixinViews, self.check_object_permissions(self.request, note) return note + + +class EstablishmentEmployeeCreateView(generics.CreateAPIView): + serializer_class = serializers.EstablishmentEmployeeCreateSerializer + queryset = models.EstablishmentEmployee.objects.all() + # TODO send email to all admins and add endpoint for changing status + + +class EstablishmentEmployeeDeleteView(generics.DestroyAPIView): + + def _get_object_to_delete(self, establishment_id, employee_id): + result_qs = models.EstablishmentEmployee\ + .objects\ + .filter(establishment_id=establishment_id, employee_id=employee_id) + if not result_qs.exists(): + raise Http404 + return result_qs.first() + + def delete(self, request, *args, **kwargs): + establishment_id = self.kwargs["establishment_id"] + employee_id = self.kwargs["employee_id"] + object_to_delete = self._get_object_to_delete(establishment_id, employee_id) + object_to_delete.delete() + return HttpResponse(status=status.HTTP_204_NO_CONTENT) From c2bb9f0814d6bbcc41f65966c75108469edcdca0 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 18:21:37 +0300 Subject: [PATCH 16/26] boost detail establishment speed --- apps/establishment/models.py | 6 ++++-- project/settings/production.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index f0260cf9..edf8d33d 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -118,11 +118,13 @@ class EstablishmentQuerySet(models.QuerySet): 'address__city__country') def with_extended_related(self): - return self.select_related('establishment_type'). \ + return self.with_extended_address_related().select_related('establishment_type'). \ prefetch_related('establishment_subtypes', 'awards', 'schedule', - 'phones'). \ + 'phones', 'gallery', 'menu_set', 'menu_set__plate_set', + 'menu_set__plate_set__currency', 'currency'). \ prefetch_actual_employees() + def with_type_related(self): return self.prefetch_related('establishment_subtypes') diff --git a/project/settings/production.py b/project/settings/production.py index 3192acea..e745f3bc 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -51,4 +51,6 @@ GUESTONLINE_SERVICE = 'https://api.guestonline.fr/' GUESTONLINE_TOKEN = '' LASTABLE_SERVICE = '' LASTABLE_TOKEN = '' -LASTABLE_PROXY = '' \ No newline at end of file +LASTABLE_PROXY = '' + +THUMBNAIL_FORCE_OVERWRITE = True # see: https://github.com/jazzband/sorl-thumbnail/issues/351 \ No newline at end of file From 586f2a7a0356fb247499c7c18db523ca60eeb6e2 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 22 Nov 2019 18:21:37 +0300 Subject: [PATCH 17/26] boost detail establishment speed (cherry picked from commit c2bb9f0) --- apps/establishment/models.py | 6 ++++-- project/settings/production.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index d22aebaa..cb490aa4 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -119,11 +119,13 @@ class EstablishmentQuerySet(models.QuerySet): 'address__city__country') def with_extended_related(self): - return self.select_related('establishment_type'). \ + return self.with_extended_address_related().select_related('establishment_type'). \ prefetch_related('establishment_subtypes', 'awards', 'schedule', - 'phones'). \ + 'phones', 'gallery', 'menu_set', 'menu_set__plate_set', + 'menu_set__plate_set__currency', 'currency'). \ prefetch_actual_employees() + def with_type_related(self): return self.prefetch_related('establishment_subtypes') diff --git a/project/settings/production.py b/project/settings/production.py index 3192acea..e745f3bc 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -51,4 +51,6 @@ GUESTONLINE_SERVICE = 'https://api.guestonline.fr/' GUESTONLINE_TOKEN = '' LASTABLE_SERVICE = '' LASTABLE_TOKEN = '' -LASTABLE_PROXY = '' \ No newline at end of file +LASTABLE_PROXY = '' + +THUMBNAIL_FORCE_OVERWRITE = True # see: https://github.com/jazzband/sorl-thumbnail/issues/351 \ No newline at end of file From c83c07b9e1384795bfe9dd8c4f500c62fcdcdc90 Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Fri, 22 Nov 2019 18:33:39 +0300 Subject: [PATCH 18/26] add site features inline admin --- apps/main/admin.py | 6 ++++++ make_data_migration.sh | 14 ++++++++++++++ project/settings/local.py | 11 ++++++----- 3 files changed, 26 insertions(+), 5 deletions(-) create mode 100755 make_data_migration.sh diff --git a/apps/main/admin.py b/apps/main/admin.py index 9ec76164..4b7038e7 100644 --- a/apps/main/admin.py +++ b/apps/main/admin.py @@ -3,9 +3,15 @@ from django.contrib import admin from main import models +class SiteSettingsInline(admin.TabularInline): + model = models.SiteFeature + extra = 1 + + @admin.register(models.SiteSettings) class SiteSettingsAdmin(admin.ModelAdmin): """Site settings admin conf.""" + inlines = [SiteSettingsInline,] @admin.register(models.Feature) diff --git a/make_data_migration.sh b/make_data_migration.sh new file mode 100755 index 00000000..c92f74e7 --- /dev/null +++ b/make_data_migration.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +./manage.py transfer -a +#./manage.py transfer -d +./manage.py transfer -e +./manage.py transfer --fill_city_gallery +./manage.py transfer -l +./manage.py transfer --product +./manage.py transfer --souvenir +./manage.py transfer --establishment_note +./manage.py transfer --product_note +./manage.py transfer --wine_characteristics +./manage.py transfer --inquiries +./manage.py transfer --assemblage +./manage.py transfer --purchased_plaques \ No newline at end of file diff --git a/project/settings/local.py b/project/settings/local.py index 959e6149..f88a72b1 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -80,11 +80,11 @@ LOGGING = { 'py.warnings': { 'handlers': ['console'], }, - 'django.db.backends': { - 'handlers': ['console', ], - 'level': 'DEBUG', - 'propagate': False, - }, + # 'django.db.backends': { + # 'handlers': ['console', ], + # 'level': 'DEBUG', + # 'propagate': False, + # }, } } @@ -106,3 +106,4 @@ ELASTICSEARCH_INDEX_NAMES = { TESTING = sys.argv[1:2] == ['test'] if TESTING: ELASTICSEARCH_INDEX_NAMES = {} +ELASTICSEARCH_DSL_AUTOSYNC = False \ No newline at end of file From 65bb22bb218c6ff8e720b0c93a5c920e9b7ada82 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 22 Nov 2019 19:04:40 +0300 Subject: [PATCH 19/26] reassign sorl-thumbnail engine --- apps/gallery/serializers.py | 24 ++++++++++++------------ apps/utils/models.py | 9 +++++---- apps/utils/thumbnail_engine.py | 19 +++++++++++++++++++ project/settings/base.py | 1 + 4 files changed, 37 insertions(+), 16 deletions(-) create mode 100644 apps/utils/thumbnail_engine.py diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index 2c2e50d0..538988c9 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -39,11 +39,11 @@ class ImageSerializer(serializers.ModelSerializer): class CropImageSerializer(ImageSerializer): """Serializers for image crops.""" - width = serializers.IntegerField(write_only=True) - height = serializers.IntegerField(write_only=True) - margin = serializers.CharField(write_only=True, allow_null=True, - required=False, - default='center') + width = serializers.IntegerField(write_only=True, required=False) + height = serializers.IntegerField(write_only=True, required=False) + crop = serializers.CharField(write_only=True, allow_null=True, + required=False, + default='center') quality = serializers.IntegerField(write_only=True, allow_null=True, required=False, default=settings.THUMBNAIL_QUALITY, validators=[ @@ -59,7 +59,7 @@ class CropImageSerializer(ImageSerializer): 'orientation_display', 'width', 'height', - 'margin', + 'crop', 'quality', 'cropped_image', ] @@ -69,16 +69,16 @@ class CropImageSerializer(ImageSerializer): file = self._image.image crop_width = attrs.get('width') crop_height = attrs.get('height') - margin = attrs.get('margin') + crop = attrs.get('crop') - if crop_height and crop_width and margin: + if crop_height and crop_width and crop: xy_image = (file.width, file.width) xy_window = (crop_width, crop_height) try: - parse_crop(margin, xy_image, xy_window) + parse_crop(crop, xy_image, xy_window) attrs['image'] = file except ThumbnailParseError: - raise serializers.ValidationError({'margin': _('Unrecognized crop option: %s') % margin}) + raise serializers.ValidationError({'margin': _('Unrecognized crop option: %s') % crop}) return attrs def create(self, validated_data): @@ -86,7 +86,7 @@ class CropImageSerializer(ImageSerializer): width = validated_data.pop('width', None) height = validated_data.pop('height', None) quality = validated_data.pop('quality') - margin = validated_data.pop('margin') + crop = validated_data.pop('crop') image = self._image @@ -96,7 +96,7 @@ class CropImageSerializer(ImageSerializer): image.get_cropped_image( geometry=f'{width}x{height}', quality=quality, - margin=margin)) + crop=crop)) return image @property diff --git a/apps/utils/models.py b/apps/utils/models.py index 59ec2282..adbe19d0 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -11,11 +11,11 @@ from django.contrib.postgres.fields.jsonb import KeyTextTransform from django.utils import timezone from django.utils.html import mark_safe from django.utils.translation import ugettext_lazy as _, get_language -from configuration.models import TranslationSettings from easy_thumbnails.fields import ThumbnailerImageField from sorl.thumbnail import get_thumbnail from sorl.thumbnail.fields import ImageField as SORLImageField +from configuration.models import TranslationSettings from utils.methods import image_path, svg_image_path from utils.validators import svg_image_validator @@ -227,16 +227,16 @@ class SORLImageMixin(models.Model): else: return None - def get_cropped_image(self, geometry: str, quality: int, margin: str) -> dict: + def get_cropped_image(self, geometry: str, quality: int, crop: str) -> dict: cropped_image = get_thumbnail(self.image, geometry_string=geometry, - crop=margin, + crop=crop, quality=quality) return { 'geometry_string': geometry, 'crop_url': cropped_image.url, 'quality': quality, - 'margin': margin + 'crop': crop } image_tag.short_description = _('Image') @@ -455,4 +455,5 @@ class FavoritesMixin: def favorites_for_users(self): return self.favorites.aggregate(arr=ArrayAgg('user_id')).get('arr') + timezone.datetime.now().date().isoformat() \ No newline at end of file diff --git a/apps/utils/thumbnail_engine.py b/apps/utils/thumbnail_engine.py new file mode 100644 index 00000000..f55d58f8 --- /dev/null +++ b/apps/utils/thumbnail_engine.py @@ -0,0 +1,19 @@ +"""Overridden thumbnail engine.""" +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.crop(image, geometry, options) + image = self.rounded(image, geometry, options) + image = self.blur(image, geometry, options) + image = self.padding(image, geometry, options) + return image diff --git a/project/settings/base.py b/project/settings/base.py index 0868c116..ca4232ae 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -520,3 +520,4 @@ ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood', NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership'] INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next'] +THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine' From d9743d84657d069aec4953a569970c539e1b7bc0 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 22 Nov 2019 22:47:41 +0300 Subject: [PATCH 20/26] refactored serializer --- apps/gallery/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index 538988c9..36360180 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -41,10 +41,10 @@ class CropImageSerializer(ImageSerializer): width = serializers.IntegerField(write_only=True, required=False) height = serializers.IntegerField(write_only=True, required=False) - crop = serializers.CharField(write_only=True, allow_null=True, + crop = serializers.CharField(write_only=True, required=False, default='center') - quality = serializers.IntegerField(write_only=True, allow_null=True, required=False, + quality = serializers.IntegerField(write_only=True, required=False, default=settings.THUMBNAIL_QUALITY, validators=[ MinValueValidator(1), @@ -71,7 +71,7 @@ class CropImageSerializer(ImageSerializer): crop_height = attrs.get('height') crop = attrs.get('crop') - if crop_height and crop_width and crop: + if (crop_height and crop_width) and (crop and crop != 'smart'): xy_image = (file.width, file.width) xy_window = (crop_width, crop_height) try: From 9b61ff31147043edbe331315db88d6bba3593dc2 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sat, 23 Nov 2019 14:58:49 +0300 Subject: [PATCH 21/26] added COOKIE_DOMAIN parameter to settings --- apps/utils/views.py | 22 ++++++---------------- project/settings/base.py | 2 ++ project/settings/development.py | 2 ++ project/settings/local.py | 11 +---------- project/settings/production.py | 6 +----- project/settings/stage.py | 4 ++-- 6 files changed, 14 insertions(+), 33 deletions(-) diff --git a/apps/utils/views.py b/apps/utils/views.py index c09df2a2..8becc9ea 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -70,22 +70,12 @@ class JWTGenericViewMixin: def _put_cookies_in_response(self, cookies: list, response: Response): """Update COOKIES in response from namedtuple""" for cookie in cookies: - # todo: remove config for develop - from os import environ - configuration = environ.get('SETTINGS_CONFIGURATION', None) - if configuration == 'development' or configuration == 'stage': - response.set_cookie(key=cookie.key, - value=cookie.value, - secure=cookie.secure, - httponly=cookie.http_only, - max_age=cookie.max_age, - domain='.id-east.ru') - else: - response.set_cookie(key=cookie.key, - value=cookie.value, - secure=cookie.secure, - httponly=cookie.http_only, - max_age=cookie.max_age, ) + response.set_cookie(key=cookie.key, + value=cookie.value, + secure=cookie.secure, + httponly=cookie.http_only, + max_age=cookie.max_age, + domain=settings.COOKIE_DOMAIN) return response def _get_tokens_from_cookies(self, request, cookies: dict = None): diff --git a/project/settings/base.py b/project/settings/base.py index ca4232ae..2c6f0cf0 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -521,3 +521,5 @@ NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', ' INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next'] THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine' + +COOKIE_DOMAIN = None diff --git a/project/settings/development.py b/project/settings/development.py index 057438f5..f850aad7 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -71,3 +71,5 @@ INSTALLED_APPS.append('transfer.apps.TransferConfig') BROKER_URL = 'redis://localhost:6379/1' CELERY_RESULT_BACKEND = BROKER_URL CELERY_BROKER_URL = BROKER_URL + +COOKIE_DOMAIN = '.id-east.ru' diff --git a/project/settings/local.py b/project/settings/local.py index f88a72b1..7784be90 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -5,18 +5,15 @@ import sys ALLOWED_HOSTS = ['*', ] - SEND_SMS = False SMS_CODE_SHOW = True USE_CELERY = True - SCHEMA_URI = 'http' DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'testserver.com:8000' DOMAIN_URI = '0.0.0.0:8000' - # CELERY # RabbitMQ # BROKER_URL = 'amqp://rabbitmq:5672' @@ -25,20 +22,16 @@ BROKER_URL = 'redis://redis:6379/1' CELERY_RESULT_BACKEND = BROKER_URL CELERY_BROKER_URL = BROKER_URL - # MEDIA MEDIA_URL = f'{SCHEMA_URI}://{DOMAIN_URI}/{MEDIA_LOCATION}/' MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION) - # SORL thumbnails THUMBNAIL_DEBUG = True - # ADDED TRANSFER APP INSTALLED_APPS.append('transfer.apps.TransferConfig') - # DATABASES DATABASES.update({ 'legacy': { @@ -88,7 +81,6 @@ LOGGING = { } } - # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { @@ -102,8 +94,7 @@ ELASTICSEARCH_INDEX_NAMES = { 'search_indexes.documents.product': 'local_product', } - TESTING = sys.argv[1:2] == ['test'] if TESTING: ELASTICSEARCH_INDEX_NAMES = {} -ELASTICSEARCH_DSL_AUTOSYNC = False \ No newline at end of file +ELASTICSEARCH_DSL_AUTOSYNC = False diff --git a/project/settings/production.py b/project/settings/production.py index e745f3bc..997d6526 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -20,7 +20,6 @@ DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'gaultmillau.com' DOMAIN_URI = 'next.gaultmillau.com' - # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { @@ -28,20 +27,17 @@ ELASTICSEARCH_DSL = { } } - ELASTICSEARCH_INDEX_NAMES = { 'search_indexes.documents.news': 'development_news', # temporarily disabled 'search_indexes.documents.establishment': 'development_establishment', 'search_indexes.documents.product': 'development_product', } - sentry_sdk.init( dsn="https://35d9bb789677410ab84a822831c6314f@sentry.io/1729093", integrations=[DjangoIntegration()] ) - BROKER_URL = 'redis://redis:6379/1' CELERY_RESULT_BACKEND = BROKER_URL CELERY_BROKER_URL = BROKER_URL @@ -53,4 +49,4 @@ LASTABLE_SERVICE = '' LASTABLE_TOKEN = '' LASTABLE_PROXY = '' -THUMBNAIL_FORCE_OVERWRITE = True # see: https://github.com/jazzband/sorl-thumbnail/issues/351 \ No newline at end of file +COOKIE_DOMAIN = '.gaultmillau.com' diff --git a/project/settings/stage.py b/project/settings/stage.py index 49a7ae0f..95285034 100644 --- a/project/settings/stage.py +++ b/project/settings/stage.py @@ -13,7 +13,6 @@ DEFAULT_SUBDOMAIN = 'www' SITE_DOMAIN_URI = 'id-east.ru' DOMAIN_URI = 'gm-stage.id-east.ru' - # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { @@ -21,8 +20,9 @@ ELASTICSEARCH_DSL = { } } - ELASTICSEARCH_INDEX_NAMES = { # 'search_indexes.documents.news': 'stage_news', #temporarily disabled 'search_indexes.documents.establishment': 'stage_establishment', } + +COOKIE_DOMAIN = '.id-east.ru' From 49e5e4ea9141a035627c4aaf829e270dfd77d8a3 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 25 Nov 2019 09:38:50 +0300 Subject: [PATCH 22/26] test establishment and news carousel --- apps/establishment/tests.py | 25 +++++++++++++++++++++++++ apps/news/tests.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index 3534608c..6d456a45 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -441,3 +441,28 @@ class EstablishmentWebFavoriteTests(ChildTestCase): f'/api/web/establishments/slug/{self.establishment.slug}/favorites/', format='json') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + +class EstablishmentCarouselTests(ChildTestCase): + + def test_web_carousel_CR(self): + data = { + "object_id": self.establishment.id + } + + response = self.client.post(f'/api/web/establishments/slug/{self.establishment.slug}/carousels/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.delete(f'/api/web/establishments/slug/{self.establishment.slug}/carousels/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_mobile_carousel_CR(self): + data = { + "object_id": self.establishment.id + } + + response = self.client.post(f'/api/mobile/establishments/slug/{self.establishment.slug}/carousels/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.delete(f'/api/mobile/establishments/slug/{self.establishment.slug}/carousels/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) diff --git a/apps/news/tests.py b/apps/news/tests.py index 532a6efc..a87e457a 100644 --- a/apps/news/tests.py +++ b/apps/news/tests.py @@ -30,12 +30,12 @@ class BaseTestCase(APITestCase): 'refresh_token': tokens.get('refresh_token')}) self.test_news_type = NewsType.objects.create(name="Test news type") - self.lang = Language.objects.get( + self.lang = Language.objects.create( title='Russia', locale='ru-RU' ) - self.country_ru = Country.objects.get( + self.country_ru = Country.objects.create( name={"en-GB": "Russian"} ) @@ -128,3 +128,28 @@ class NewsTestCase(BaseTestCase): response = self.client.delete(f'/api/web/news/slug/{self.test_news.slug}/favorites/', format='json') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + +class NewsCarouselTests(BaseTestCase): + + def test_web_carousel_CR(self): + data = { + "object_id": self.test_news.id + } + + response = self.client.post(f'/api/web/news/slug/{self.test_news.slug}/carousels/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.delete(f'/api/web/news/slug/{self.test_news.slug}/carousels/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_mobile_carousel_CR(self): + data = { + "object_id": self.test_news.id + } + + response = self.client.post(f'/api/mobile/news/slug/{self.test_news.slug}/carousels/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.delete(f'/api/mobile/news/slug/{self.test_news.slug}/carousels/') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) From 49c6dc8f1d31c463b9fc8dfa455733577e0b9aaa Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 25 Nov 2019 14:21:05 +0300 Subject: [PATCH 23/26] static to Amazon --- project/settings/amazon_s3.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/project/settings/amazon_s3.py b/project/settings/amazon_s3.py index c793dd77..c96da5d9 100644 --- a/project/settings/amazon_s3.py +++ b/project/settings/amazon_s3.py @@ -13,9 +13,9 @@ AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'} AWS_S3_ADDRESSING_STYLE = 'path' # Static settings -# PUBLIC_STATIC_LOCATION = 'static' -# STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/' -# STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage' +PUBLIC_STATIC_LOCATION = 'static' +STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/' +STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage' # Public media settings MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/' From a660ffcc313623d69e437afb6f27734c372f717a Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Mon, 25 Nov 2019 14:26:37 +0300 Subject: [PATCH 24/26] static to Amazon #2 --- project/settings/amazon_s3.py | 2 +- project/settings/production.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/project/settings/amazon_s3.py b/project/settings/amazon_s3.py index c96da5d9..b602618d 100644 --- a/project/settings/amazon_s3.py +++ b/project/settings/amazon_s3.py @@ -13,7 +13,7 @@ AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'} AWS_S3_ADDRESSING_STYLE = 'path' # Static settings -PUBLIC_STATIC_LOCATION = 'static' +PUBLIC_STATIC_LOCATION = 'static-dev' STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/' STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage' diff --git a/project/settings/production.py b/project/settings/production.py index 997d6526..7ef2dc62 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -4,6 +4,11 @@ from .amazon_s3 import * import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration + +PUBLIC_STATIC_LOCATION = 'static' +STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_STATIC_LOCATION}/' +STATICFILES_STORAGE = 'project.storage_backends.PublicStaticStorage' + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False From 17f67d020ab0436bda47621a190dafc841435a50 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 25 Nov 2019 15:00:53 +0300 Subject: [PATCH 25/26] back carousel api and test --- apps/establishment/serializers/common.py | 2 +- apps/establishment/tests.py | 17 +++-------------- apps/establishment/urls/back.py | 2 ++ apps/establishment/urls/common.py | 2 -- apps/news/serializers.py | 2 +- apps/news/tests.py | 17 +++-------------- apps/news/urls/back.py | 3 +-- apps/news/urls/common.py | 2 -- apps/utils/serializers.py | 4 ++-- apps/utils/views.py | 5 +++++ 10 files changed, 18 insertions(+), 38 deletions(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 9419c449..cb102ff1 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -485,7 +485,7 @@ class EstablishmentCarouselCreateSerializer(CarouselCreateSerializer): """Serializer to carousel object w/ model News.""" def validate(self, attrs): - establishment = models.Establishment.objects.filter(slug=self.slug).first() + establishment = models.Establishment.objects.filter(pk=self.pk).first() if not establishment: raise serializers.ValidationError({'detail': _('Object not found.')}) diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index 6d456a45..c2613d30 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -445,24 +445,13 @@ class EstablishmentWebFavoriteTests(ChildTestCase): class EstablishmentCarouselTests(ChildTestCase): - def test_web_carousel_CR(self): + def test_back_carousel_CR(self): data = { "object_id": self.establishment.id } - response = self.client.post(f'/api/web/establishments/slug/{self.establishment.slug}/carousels/', data=data) + response = self.client.post(f'/api/back/establishments/{self.establishment.id}/carousels/', data=data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.delete(f'/api/web/establishments/slug/{self.establishment.slug}/carousels/') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - def test_mobile_carousel_CR(self): - data = { - "object_id": self.establishment.id - } - - response = self.client.post(f'/api/mobile/establishments/slug/{self.establishment.slug}/carousels/', data=data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - response = self.client.delete(f'/api/mobile/establishments/slug/{self.establishment.slug}/carousels/') + response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/carousels/') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index a06adc3b..63bb286d 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -9,6 +9,8 @@ app_name = 'establishment' urlpatterns = [ path('', views.EstablishmentListCreateView.as_view(), name='list'), path('/', views.EstablishmentRUDView.as_view(), name='detail'), + path('/carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(), + name='create-destroy-carousels'), path('/schedule//', views.EstablishmentScheduleRUDView.as_view(), name='schedule-rud'), path('/schedule/', views.EstablishmentScheduleCreateView.as_view(), diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 3f46df90..e37c38f8 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -18,6 +18,4 @@ urlpatterns = [ name='rud-comment'), path('slug//favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), name='create-destroy-favorites'), - path('slug//carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(), - name='create-destroy-carousels') ] diff --git a/apps/news/serializers.py b/apps/news/serializers.py index f58e7c24..cc5087f6 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -275,7 +275,7 @@ class NewsCarouselCreateSerializer(CarouselCreateSerializer): """Serializer to carousel object w/ model News.""" def validate(self, attrs): - news = models.News.objects.filter(slug=self.slug).first() + news = models.News.objects.filter(pk=self.pk).first() if not news: raise serializers.ValidationError({'detail': _('Object not found.')}) diff --git a/apps/news/tests.py b/apps/news/tests.py index a87e457a..1754c15f 100644 --- a/apps/news/tests.py +++ b/apps/news/tests.py @@ -132,24 +132,13 @@ class NewsTestCase(BaseTestCase): class NewsCarouselTests(BaseTestCase): - def test_web_carousel_CR(self): + def test_back_carousel_CR(self): data = { "object_id": self.test_news.id } - response = self.client.post(f'/api/web/news/slug/{self.test_news.slug}/carousels/', data=data) + response = self.client.post(f'/api/back/news/{self.test_news.id}/carousels/', data=data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.delete(f'/api/web/news/slug/{self.test_news.slug}/carousels/') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - def test_mobile_carousel_CR(self): - data = { - "object_id": self.test_news.id - } - - response = self.client.post(f'/api/mobile/news/slug/{self.test_news.slug}/carousels/', data=data) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - response = self.client.delete(f'/api/mobile/news/slug/{self.test_news.slug}/carousels/') + response = self.client.delete(f'/api/back/news/{self.test_news.id}/carousels/') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) diff --git a/apps/news/urls/back.py b/apps/news/urls/back.py index 9126b3e9..982e7810 100644 --- a/apps/news/urls/back.py +++ b/apps/news/urls/back.py @@ -13,6 +13,5 @@ urlpatterns = [ name='gallery-list'), path('/gallery//', views.NewsBackOfficeGalleryCreateDestroyView.as_view(), name='gallery-create-destroy'), - path('slug//carousels/', views.NewsCarouselCreateDestroyView.as_view(), - name='create-destroy-carousels'), + path('/carousels/', views.NewsCarouselCreateDestroyView.as_view(), name='create-destroy-carousels'), ] diff --git a/apps/news/urls/common.py b/apps/news/urls/common.py index e2aae7a1..f5c809de 100644 --- a/apps/news/urls/common.py +++ b/apps/news/urls/common.py @@ -7,6 +7,4 @@ common_urlpatterns = [ path('slug//', views.NewsDetailView.as_view(), name='rud'), path('slug//favorites/', views.NewsFavoritesCreateDestroyView.as_view(), name='create-destroy-favorites'), - path('slug//carousels/', views.NewsCarouselCreateDestroyView.as_view(), - name='create-destroy-carousels'), ] diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index 634246b7..f55b69bc 100644 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -115,8 +115,8 @@ class CarouselCreateSerializer(serializers.ModelSerializer): return self.context.get('request') @property - def slug(self): - return self.request.parser_context.get('kwargs').get('slug') + def pk(self): + return self.request.parser_context.get('kwargs').get('pk') class RecursiveFieldSerializer(serializers.Serializer): diff --git a/apps/utils/views.py b/apps/utils/views.py index 8becc9ea..478a3cb2 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -155,6 +155,11 @@ class FavoritesCreateDestroyMixinView(BaseCreateDestroyMixinView): class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView): """Carousel Create Destroy mixin.""" + lookup_field = 'id' + + def get_base_object(self): + return get_object_or_404(self._model, id=self.kwargs['pk']) + def get_object(self): """ Returns the object the view is displaying. From 1711057ceed60eb87857d52b149fbb4e6cd43384 Mon Sep 17 00:00:00 2001 From: Semyon Yekhmenin Date: Mon, 25 Nov 2019 12:15:51 +0000 Subject: [PATCH 26/26] Feature/bo establishment employee features --- apps/establishment/serializers/back.py | 3 ++- apps/establishment/urls/back.py | 1 + apps/establishment/views/back.py | 9 +++++++++ apps/utils/decorators.py | 4 +++- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 52af0574..dd16e861 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -162,7 +162,7 @@ class ContactEmailBackSerializers(model_serializers.PlateSerializer): class EmployeeBackSerializers(serializers.ModelSerializer): """Employee serializers.""" - awards = AwardSerializer(many=True) + awards = AwardSerializer(many=True, read_only=True) class Meta: model = models.Employee @@ -209,6 +209,7 @@ class EstablishmentEmployeeBackSerializer(serializers.ModelSerializer): 'from_date', 'to_date', 'position', + 'status', ] diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index 63bb286d..d9b2fbd7 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -54,4 +54,5 @@ urlpatterns = [ path('types//', views.EstablishmentTypeRUDView.as_view(), name='type-rud'), path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'), path('subtypes//', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'), + path('positions/', views.EstablishmentPositionListView.as_view(), name='position-list'), ] diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 312bb171..d3afbf2e 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -356,3 +356,12 @@ class EstablishmentEmployeeDeleteView(generics.DestroyAPIView): object_to_delete = self._get_object_to_delete(establishment_id, employee_id) object_to_delete.delete() return HttpResponse(status=status.HTTP_204_NO_CONTENT) + + +class EstablishmentPositionListView(generics.ListAPIView): + """Establishment positions list view.""" + + pagination_class = None + permission_classes = (permissions.AllowAny, ) + queryset = models.Position.objects.all() + serializer_class = serializers.PositionBackSerializer diff --git a/apps/utils/decorators.py b/apps/utils/decorators.py index c48a26c7..18bed79b 100644 --- a/apps/utils/decorators.py +++ b/apps/utils/decorators.py @@ -1,3 +1,5 @@ +from django.contrib.auth.models import AbstractUser + def with_base_attributes(cls): @@ -8,7 +10,7 @@ def with_base_attributes(cls): if request and hasattr(request, "user"): user = request.user - if user is not None: + if user is not None and isinstance(user, AbstractUser): data.update({'modified_by': user}) if not self.instance: