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 utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField,
TranslatedFieldsMixin, get_current_locale)
TranslatedFieldsMixin, get_current_locale,
IntermediateGalleryModelMixin, GalleryModelMixin)
class CountryQuerySet(models.QuerySet):
@ -96,9 +97,8 @@ class CityQuerySet(models.QuerySet):
return self.filter(country__code=code)
class City(models.Model):
class City(GalleryModelMixin):
"""Region model."""
name = models.CharField(_('name'), max_length=250)
code = models.CharField(_('code'), max_length=250)
region = models.ForeignKey(
@ -111,6 +111,8 @@ class City(models.Model):
is_island = models.BooleanField(_('is island'), default=False)
old_id = models.IntegerField(null=True, blank=True, default=None)
gallery = models.ManyToManyField('gallery.Image', through='CityGallery')
objects = CityQuerySet.as_manager()
class Meta:
@ -121,6 +123,24 @@ class City(models.Model):
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):
"""Address model."""
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):
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):
"""Wine region model."""

View File

@ -1,5 +1,8 @@
from location import models
from location.serializers import common
from rest_framework import serializers
from gallery.models import Image
from django.utils.translation import gettext_lazy as _
class AddressCreateSerializer(common.AddressDetailSerializer):
@ -18,3 +21,45 @@ class CountryBackSerializer(common.CountrySerializer):
'name',
'country_id'
]
class CityGallerySerializer(serializers.ModelSerializer):
"""Serializer class for model CityGallery."""
class Meta:
"""Meta class"""
model = models.CityGallery
fields = [
'id',
'is_main',
]
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/<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/<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.views import common
from utils.permissions import IsCountryAdmin
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from utils.views import CreateDestroyGalleryViewMixin
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django.shortcuts import get_object_or_404
from utils.serializers import ImageBaseSerializer
# Address
@ -35,6 +39,48 @@ class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class CityGalleryCreateDestroyView(common.CityViewMixin,
CreateDestroyGalleryViewMixin):
"""Resource for a create gallery for product for back-office users."""
serializer_class = serializers.CityGallerySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
def get_object(self):
"""
Returns the object the view is displaying.
"""
city_qs = self.filter_queryset(self.get_queryset())
city = get_object_or_404(city_qs, pk=self.kwargs['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
class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region"""

View File

@ -68,7 +68,7 @@ class WineRegionListView(generics.ListAPIView):
pagination_class = None
model = models.WineRegion
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

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

@ -153,7 +153,7 @@ class Award(TranslatedFieldsMixin, URLImageMixin, models.Model):
PUBLISHED = 1
STATE_CHOICES = (
(WAITING,'waiting'),
(WAITING, 'waiting'),
(PUBLISHED, 'published')
)

View File

@ -67,8 +67,7 @@ class Command(BaseCommand):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select
DISTINCT
m.id as old_id,
DISTINCT
trim(CONVERT(m.value USING utf8)) as tag_value,
trim(CONVERT(v.key_name USING utf8)) as tag_category
FROM product_metadata m
@ -103,6 +102,14 @@ class Command(BaseCommand):
p.tags.clear()
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):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
@ -145,6 +152,7 @@ class Command(BaseCommand):
def handle(self, *args, **kwargs):
self.remove_tags_product()
self.remove_tags()
self.add_category_tag()
self.add_type_product_category()
self.add_tag()

View File

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

View File

@ -17,7 +17,6 @@ class ProductBaseView(generics.GenericAPIView):
return Product.objects.published() \
.with_base_related() \
.annotate_in_favorites(self.request.user) \
.by_country_code(self.request.country_code) \
.order_by('-created')
@ -27,7 +26,8 @@ class ProductListView(ProductBaseView, generics.ListAPIView):
filter_class = filters.ProductFilterSet
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

View File

@ -8,6 +8,7 @@ class ReviewFilter(filters.FilterSet):
"""Review filter set."""
establishment_id = filters.NumberFilter(field_name='object_id', )
product_id = filters.NumberFilter(field_name='object_id', )
class Meta:
"""Meta class."""
@ -15,9 +16,15 @@ class ReviewFilter(filters.FilterSet):
model = models.Review
fields = (
'establishment_id',
'product_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

View File

@ -34,7 +34,7 @@ def transfer_languages():
def transfer_reviews():
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),
).values('id', 'reviewer_id', 'aasm_state', 'created_at', 'establishment_id', 'mark', 'vintage')

View File

@ -33,8 +33,38 @@ class ProductDocument(Document):
properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
'index_name': 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(

View File

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

View File

@ -95,14 +95,6 @@ class ProductTypeDocumentSerializer(serializers.Serializer):
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):
"""City serializer for ES Document,"""
@ -111,6 +103,39 @@ class CityDocumentShortSerializer(serializers.Serializer):
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):
"""Address serializer for ES Document."""

View File

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

View File

@ -44,7 +44,7 @@ class TagCategoryFilterSet(TagsBaseFilterSet):
def by_product_type(self, queryset, name, value):
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)
return queryset

View File

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

View File

@ -238,6 +238,10 @@ class SVGImageMixin(models.Model):
validators=[svg_image_validator, ],
verbose_name=_('SVG image'))
@property
def svg_image_indexing(self):
return self.svg_image.url if self.svg_image else None
class Meta:
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']
INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
#ELASTICSEARCH_DSL_AUTOSYNC = False
ELASTICSEARCH_DSL_AUTOSYNC = False