Merge branch 'develop' of ssh://gl.id-east.ru:222/gm/gm-backend into develop

This commit is contained in:
Dmitriy Kuzmenko 2019-11-20 16:29:57 +03:00
commit 15ddbd5a7e
21 changed files with 306 additions and 26 deletions

View File

@ -0,0 +1,34 @@
# Generated by Django 2.2.7 on 2019-11-20 10:10
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('gallery', '0006_merge_20191027_1758'),
('location', '0029_merge_20191119_1438'),
]
operations = [
migrations.CreateModel(
name='CityGallery',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_main', models.BooleanField(default=False, verbose_name='Is the main image')),
('city', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='city_gallery', to='location.City', verbose_name='city')),
('image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='city_gallery', to='gallery.Image', verbose_name='image')),
],
options={
'verbose_name': 'city gallery',
'verbose_name_plural': 'city galleries',
'unique_together': {('city', 'is_main'), ('city', 'image')},
},
),
migrations.AddField(
model_name='city',
name='gallery',
field=models.ManyToManyField(through='location.CityGallery', to='gallery.Image'),
),
]

View File

@ -8,7 +8,8 @@ from django.utils.translation import gettext_lazy as _
from translation.models import Language from translation.models import Language
from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField, from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField,
TranslatedFieldsMixin, get_current_locale) TranslatedFieldsMixin, get_current_locale,
IntermediateGalleryModelMixin, GalleryModelMixin)
class CountryQuerySet(models.QuerySet): class CountryQuerySet(models.QuerySet):
@ -96,9 +97,8 @@ class CityQuerySet(models.QuerySet):
return self.filter(country__code=code) return self.filter(country__code=code)
class City(models.Model): class City(GalleryModelMixin):
"""Region model.""" """Region model."""
name = models.CharField(_('name'), max_length=250) name = models.CharField(_('name'), max_length=250)
code = models.CharField(_('code'), max_length=250) code = models.CharField(_('code'), max_length=250)
region = models.ForeignKey( region = models.ForeignKey(
@ -111,6 +111,8 @@ class City(models.Model):
is_island = models.BooleanField(_('is island'), default=False) is_island = models.BooleanField(_('is island'), default=False)
old_id = models.IntegerField(null=True, blank=True, default=None) old_id = models.IntegerField(null=True, blank=True, default=None)
gallery = models.ManyToManyField('gallery.Image', through='CityGallery')
objects = CityQuerySet.as_manager() objects = CityQuerySet.as_manager()
class Meta: class Meta:
@ -121,6 +123,24 @@ class City(models.Model):
return self.name return self.name
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): class Address(models.Model):
"""Address model.""" """Address model."""
city = models.ForeignKey(City, verbose_name=_('city'), on_delete=models.CASCADE) city = models.ForeignKey(City, verbose_name=_('city'), on_delete=models.CASCADE)
@ -172,6 +192,10 @@ class WineRegionQuerySet(models.QuerySet):
def with_sub_region_related(self): def with_sub_region_related(self):
return self.prefetch_related('wine_sub_region') return self.prefetch_related('wine_sub_region')
def having_wines(self, value = True):
"""Return qs with regions, which have any wine related to them"""
return self.exclude(wines__isnull=value)
class WineRegion(models.Model, TranslatedFieldsMixin): class WineRegion(models.Model, TranslatedFieldsMixin):
"""Wine region model.""" """Wine region model."""

View File

@ -1,5 +1,8 @@
from location import models from location import models
from location.serializers import common 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): class AddressCreateSerializer(common.AddressDetailSerializer):
@ -18,3 +21,45 @@ class CountryBackSerializer(common.CountrySerializer):
'name', 'name',
'country_id' 'country_id'
] ]
class CityGallerySerializer(serializers.ModelSerializer):
"""Serializer class for model CityGallery."""
class Meta:
"""Meta class"""
model = models.CityGallery
fields = [
'id',
'is_main',
]
def get_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.get_request_kwargs().get('pk')
image_id = self.get_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

View File

@ -11,6 +11,11 @@ urlpatterns = [
path('cities/', views.CityListCreateView.as_view(), name='city-list-create'), path('cities/', views.CityListCreateView.as_view(), name='city-list-create'),
path('cities/<int:pk>/', views.CityRUDView.as_view(), name='city-retrieve'), path('cities/<int:pk>/', views.CityRUDView.as_view(), name='city-retrieve'),
path('cities/<int:pk>/gallery/', views.CityGalleryListView.as_view(),
name='gallery-list'),
path('cities/<int:pk>/gallery/<int:image_id>/',
views.CityGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'),
path('countries/', views.CountryListCreateView.as_view(), name='country-list-create'), path('countries/', views.CountryListCreateView.as_view(), name='country-list-create'),
path('countries/<int:pk>/', views.CountryRUDView.as_view(), name='country-retrieve'), path('countries/<int:pk>/', views.CountryRUDView.as_view(), name='country-retrieve'),

View File

@ -4,7 +4,11 @@ from rest_framework import generics
from location import models, serializers from location import models, serializers
from location.views import common from location.views import common
from utils.permissions import IsCountryAdmin from utils.permissions import IsCountryAdmin
from utils.views import CreateDestroyGalleryViewMixin
from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django.shortcuts import get_object_or_404
from utils.serializers import ImageBaseSerializer
# Address # Address
@ -35,6 +39,48 @@ class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] 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['pk'])
gallery = get_object_or_404(city.city_gallery, image_id=self.kwargs['image_id'])
# May raise a permission denied
self.check_object_permissions(self.request, gallery)
return gallery
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 # Region
class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView): class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region""" """Create view for model Region"""

View File

@ -68,7 +68,7 @@ class WineRegionListView(generics.ListAPIView):
pagination_class = None pagination_class = None
model = models.WineRegion model = models.WineRegion
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
queryset = models.WineRegion.objects.with_sub_region_related().all() queryset = models.WineRegion.objects.with_sub_region_related().having_wines()
serializer_class = serializers.WineRegionSerializer serializer_class = serializers.WineRegionSerializer

37
apps/main/filters.py Normal file
View File

@ -0,0 +1,37 @@
from django.core.validators import EMPTY_VALUES
from django_filters import rest_framework as filters
from review import models
class AwardFilter(filters.FilterSet):
"""Award filter set."""
establishment_id = filters.NumberFilter(field_name='object_id', )
product_id = filters.NumberFilter(field_name='object_id', )
employee_id = filters.NumberFilter(field_name='object_id', )
class Meta:
"""Meta class."""
model = models.Review
fields = (
'establishment_id',
'product_id',
'employee_id',
)
def by_establishment_id(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_establishment_id(value, content_type='establishment')
return queryset
def by_product_id(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_product_id(value, content_type='product')
return queryset
def by_employee_id(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_employee_id(value, content_type='establishmentemployee')
return queryset

View File

@ -68,7 +68,6 @@ class Command(BaseCommand):
cursor.execute(''' cursor.execute('''
select select
DISTINCT DISTINCT
m.id as old_id,
trim(CONVERT(m.value USING utf8)) as tag_value, trim(CONVERT(m.value USING utf8)) as tag_value,
trim(CONVERT(v.key_name USING utf8)) as tag_category trim(CONVERT(v.key_name USING utf8)) as tag_category
FROM product_metadata m FROM product_metadata m
@ -103,6 +102,14 @@ class Command(BaseCommand):
p.tags.clear() p.tags.clear()
print('End clear tags product') print('End clear tags product')
def remove_tags(self):
print('Begin delete many tags')
Tag.objects.\
filter(news__isnull=True, establishments__isnull=True).delete()
print('End delete many tags')
def product_sql(self): def product_sql(self):
with connections['legacy'].cursor() as cursor: with connections['legacy'].cursor() as cursor:
cursor.execute(''' cursor.execute('''
@ -145,6 +152,7 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
self.remove_tags_product() self.remove_tags_product()
self.remove_tags()
self.add_category_tag() self.add_category_tag()
self.add_type_product_category() self.add_type_product_category()
self.add_tag() self.add_tag()

View File

@ -265,8 +265,10 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, HasTagsM
@property @property
def related_tags(self): def related_tags(self):
return super().visible_tags.exclude(category__index_name__in=['sugar-content', 'wine-color', return super().visible_tags.exclude(category__index_name__in=[
'bottles-produced','serial-number', 'grape-variety']) 'sugar-content', 'wine-color', 'bottles-produced',
'serial-number', 'grape-variety']
)
@property @property
def display_name(self): def display_name(self):
@ -363,7 +365,7 @@ class ProductStandard(models.Model):
class ProductGallery(IntermediateGalleryModelMixin): class ProductGallery(IntermediateGalleryModelMixin):
"""Gallery for model Product."""
product = models.ForeignKey(Product, null=True, product = models.ForeignKey(Product, null=True,
related_name='product_gallery', related_name='product_gallery',
on_delete=models.CASCADE, on_delete=models.CASCADE,

View File

@ -17,7 +17,6 @@ class ProductBaseView(generics.GenericAPIView):
return Product.objects.published() \ return Product.objects.published() \
.with_base_related() \ .with_base_related() \
.annotate_in_favorites(self.request.user) \ .annotate_in_favorites(self.request.user) \
.by_country_code(self.request.country_code) \
.order_by('-created') .order_by('-created')
@ -27,7 +26,8 @@ class ProductListView(ProductBaseView, generics.ListAPIView):
filter_class = filters.ProductFilterSet filter_class = filters.ProductFilterSet
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset().with_extended_related() qs = super().get_queryset().with_extended_related() \
.by_country_code(self.request.country_code)
return qs return qs

View File

@ -8,6 +8,7 @@ class ReviewFilter(filters.FilterSet):
"""Review filter set.""" """Review filter set."""
establishment_id = filters.NumberFilter(field_name='object_id', ) establishment_id = filters.NumberFilter(field_name='object_id', )
product_id = filters.NumberFilter(field_name='object_id', )
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -15,9 +16,15 @@ class ReviewFilter(filters.FilterSet):
model = models.Review model = models.Review
fields = ( fields = (
'establishment_id', 'establishment_id',
'product_id',
) )
def by_establishment_id(self, queryset, name, value): def by_establishment_id(self, queryset, name, value):
if value not in EMPTY_VALUES: if value not in EMPTY_VALUES:
return queryset.by_establishment_id(value, content_type='establishment') return queryset.by_establishment_id(value, content_type='establishment')
return queryset return queryset
def by_product_id(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_product_id(value, content_type='product')
return queryset

View File

@ -34,7 +34,7 @@ def transfer_languages():
def transfer_reviews(): def transfer_reviews():
establishments = Establishment.objects.filter(old_id__isnull=False).values_list('old_id', flat=True) establishments = Establishment.objects.filter(old_id__isnull=False).values_list('old_id', flat=True)
queryset = Reviews.objects.filter( queryset = Reviews.objects.exclude(product_id__isnull=False).filter(
establishment_id__in=list(establishments), establishment_id__in=list(establishments),
).values('id', 'reviewer_id', 'aasm_state', 'created_at', 'establishment_id', 'mark', 'vintage') ).values('id', 'reviewer_id', 'aasm_state', 'created_at', 'establishment_id', 'mark', 'vintage')

View File

@ -33,8 +33,38 @@ class ProductDocument(Document):
properties={ properties={
'id': fields.IntegerField(), 'id': fields.IntegerField(),
'name': fields.KeywordField(), 'name': fields.KeywordField(),
'index_name': fields.KeywordField(),
'slug': fields.KeywordField(), 'slug': fields.KeywordField(),
# 'city' TODO: city indexing 'city': fields.ObjectField(
attr='address.city',
properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
'code': fields.KeywordField(),
'country': fields.ObjectField(
properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'code': fields.KeywordField(),
'svg_image': fields.KeywordField(attr='svg_image_indexing')
}
),
}
),
'address': fields.ObjectField(
properties={
'city': fields.ObjectField(
properties={
'country': fields.ObjectField(
properties={
'code': fields.KeywordField()
}
)
}
)
}
)
} }
) )
wine_colors = fields.ObjectField( wine_colors = fields.ObjectField(

View File

@ -70,10 +70,16 @@ class CustomSearchFilterBackend(SearchFilterBackend):
__queries.append( __queries.append(
Q("match", **{k: v}) Q("match", **{k: v})
) )
__queries.append(
Q('wildcard', **{k: f'*{search_term.lower()}*'})
)
else: else:
__queries.append( __queries.append(
Q("match", **field_kwargs) Q("match", **field_kwargs)
) )
__queries.append(
Q('wildcard', **{field: f'*{search_term.lower()}*'})
)
else: else:
for field in view.search_fields: for field in view.search_fields:
# Initial kwargs for the match query # Initial kwargs for the match query
@ -92,8 +98,14 @@ class CustomSearchFilterBackend(SearchFilterBackend):
__queries.append( __queries.append(
Q("match", **{k: v}) Q("match", **{k: v})
) )
__queries.append(
Q('wildcard', **{k: f'*{search_term.lower()}*'})
)
else: else:
__queries.append( __queries.append(
Q("match", **field_kwargs) Q("match", **field_kwargs)
) )
__queries.append(
Q('wildcard', **{field: f'*{search_term.lower()}*'})
)
return __queries return __queries

View File

@ -95,14 +95,6 @@ class ProductTypeDocumentSerializer(serializers.Serializer):
return get_translated_value(obj.name) return get_translated_value(obj.name)
class ProductEstablishmentDocumentSerializer(serializers.Serializer):
"""Related to Product Establishment ES document serializer."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
class CityDocumentShortSerializer(serializers.Serializer): class CityDocumentShortSerializer(serializers.Serializer):
"""City serializer for ES Document,""" """City serializer for ES Document,"""
@ -111,6 +103,39 @@ class CityDocumentShortSerializer(serializers.Serializer):
name = serializers.CharField() name = serializers.CharField()
class CountryDocumentSerializer(serializers.Serializer):
id = serializers.IntegerField()
code = serializers.CharField(allow_null=True)
svg_image = serializers.CharField()
name_translated = serializers.SerializerMethodField()
@staticmethod
def get_name_translated(obj):
return get_translated_value(obj.name)
class AnotherCityDocumentShortSerializer(CityDocumentShortSerializer):
country = CountryDocumentSerializer()
def to_representation(self, instance):
if instance != AttrDict(d={}) or \
(isinstance(instance, dict) and len(instance) != 0):
return super().to_representation(instance)
return None
class ProductEstablishmentDocumentSerializer(serializers.Serializer):
"""Related to Product Establishment ES document serializer."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
index_name = serializers.CharField()
city = AnotherCityDocumentShortSerializer()
class AddressDocumentSerializer(serializers.Serializer): class AddressDocumentSerializer(serializers.Serializer):
"""Address serializer for ES Document.""" """Address serializer for ES Document."""

View File

@ -243,7 +243,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
'lookups': [constants.LOOKUP_QUERY_IN], 'lookups': [constants.LOOKUP_QUERY_IN],
}, },
'country': { 'country': {
'field': 'wine_region.country.code', 'field': 'establishment.address.city.country.code',
}, },
'wine_colors_id': { 'wine_colors_id': {
'field': 'wine_colors.id', 'field': 'wine_colors.id',

View File

@ -44,7 +44,7 @@ class TagCategoryFilterSet(TagsBaseFilterSet):
def by_product_type(self, queryset, name, value): def by_product_type(self, queryset, name, value):
if value == product_models.ProductType.WINE: if value == product_models.ProductType.WINE:
queryset = queryset.filter(index_name='wine-color') queryset = queryset.filter(index_name='wine-color').filter(tags__products__isnull=False)
queryset = queryset.by_product_type(value) queryset = queryset.by_product_type(value)
return queryset return queryset

View File

@ -69,6 +69,7 @@ class ProductReviewSerializer(ReviewSerializer):
data.pop('reviewer_id') data.pop('reviewer_id')
data.pop('product_id') data.pop('product_id')
data.pop('aasm_state') data.pop('aasm_state')
data.pop('establishment_id')
return data return data
def create(self, validated_data): def create(self, validated_data):

View File

@ -238,6 +238,10 @@ class SVGImageMixin(models.Model):
validators=[svg_image_validator, ], validators=[svg_image_validator, ],
verbose_name=_('SVG image')) verbose_name=_('SVG image'))
@property
def svg_image_indexing(self):
return self.svg_image.url if self.svg_image else None
class Meta: class Meta:
abstract = True abstract = True

View File

@ -503,4 +503,4 @@ ESTABLISHMENT_CHOSEN_TAGS = ['gastronomic', 'en_vogue', 'terrace', 'streetfood',
NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership'] NEWS_CHOSEN_TAGS = ['eat', 'drink', 'cook', 'style', 'international', 'event', 'partnership']
INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next'] INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
#ELASTICSEARCH_DSL_AUTOSYNC = False ELASTICSEARCH_DSL_AUTOSYNC = False