Merge branch 'develop' into feature/fix-country-region-city-transfer
This commit is contained in:
commit
1b7c83899a
|
|
@ -1,3 +1,4 @@
|
|||
FROM mdillon/postgis:10
|
||||
RUN localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8
|
||||
ENV LANG ru_RU.utf8
|
||||
COPY hstore.sql /docker-entrypoint-initdb.d
|
||||
1
_dockerfiles/db/hstore.sql
Normal file
1
_dockerfiles/db/hstore.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
create extension hstore;
|
||||
20
apps/account/migrations/0024_role_establishment_subtype.py
Normal file
20
apps/account/migrations/0024_role_establishment_subtype.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 2.2.7 on 2019-12-06 06:55
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('establishment', '0067_auto_20191122_1244'),
|
||||
('account', '0023_auto_20191204_0916'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='role',
|
||||
name='establishment_subtype',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='establishment.EstablishmentSubType', verbose_name='Establishment subtype'),
|
||||
),
|
||||
]
|
||||
23
apps/account/migrations/0025_auto_20191210_0623.py
Normal file
23
apps/account/migrations/0025_auto_20191210_0623.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.2.7 on 2019-12-10 06:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('account', '0024_role_establishment_subtype'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='city',
|
||||
field=models.TextField(blank=True, default=None, null=True, verbose_name='User last visited from city'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='locale',
|
||||
field=models.CharField(blank=True, default=None, max_length=10, null=True, verbose_name='User last used locale'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
"""Account models"""
|
||||
from datetime import datetime
|
||||
from tabnanny import verbose
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager
|
||||
|
|
@ -15,7 +16,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from authorization.models import Application
|
||||
from establishment.models import Establishment
|
||||
from establishment.models import Establishment, EstablishmentSubType
|
||||
from location.models import Country
|
||||
from main.models import SiteSettings
|
||||
from utils.models import GMTokenGenerator
|
||||
|
|
@ -33,7 +34,7 @@ class Role(ProjectBaseMixin):
|
|||
REVIEWER_MANGER = 6
|
||||
RESTAURANT_REVIEWER = 7
|
||||
SALES_MAN = 8
|
||||
WINERY_REVIEWER = 9
|
||||
WINERY_REVIEWER = 9 # Establishments subtype "winery"
|
||||
SELLER = 10
|
||||
|
||||
ROLE_CHOICES = (
|
||||
|
|
@ -54,6 +55,9 @@ class Role(ProjectBaseMixin):
|
|||
null=True, blank=True, on_delete=models.SET_NULL)
|
||||
site = models.ForeignKey(SiteSettings, verbose_name=_('Site settings'),
|
||||
null=True, blank=True, on_delete=models.SET_NULL)
|
||||
establishment_subtype = models.ForeignKey(EstablishmentSubType,
|
||||
verbose_name=_('Establishment subtype'),
|
||||
null=True, blank=True, on_delete=models.SET_NULL)
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
|
|
@ -103,6 +107,10 @@ class User(AbstractUser):
|
|||
email_confirmed = models.BooleanField(_('email status'), default=False)
|
||||
newsletter = models.NullBooleanField(default=True)
|
||||
old_id = models.IntegerField(null=True, blank=True, default=None)
|
||||
locale = models.CharField(max_length=10, blank=True, default=None, null=True,
|
||||
verbose_name=_('User last used locale'))
|
||||
city = models.TextField(default=None, blank=True, null=True,
|
||||
verbose_name=_('User last visited from city'))
|
||||
|
||||
EMAIL_FIELD = 'email'
|
||||
USERNAME_FIELD = 'username'
|
||||
|
|
|
|||
|
|
@ -33,11 +33,14 @@ class BackUserSerializer(serializers.ModelSerializer):
|
|||
'email_confirmed',
|
||||
'newsletter',
|
||||
'roles',
|
||||
'password',
|
||||
'city',
|
||||
'locale',
|
||||
)
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': True}
|
||||
'password': {'write_only': True},
|
||||
}
|
||||
read_only_fields = ('old_password', 'last_login', 'date_joined')
|
||||
read_only_fields = ('old_password', 'last_login', 'date_joined', 'city', 'locale')
|
||||
|
||||
def create(self, validated_data):
|
||||
user = super().create(validated_data)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ app_name = 'account'
|
|||
urlpatterns = [
|
||||
path('role/', views.RoleLstView.as_view(), name='role-list-create'),
|
||||
path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'),
|
||||
path('user/', views.UserLstView.as_view(), name='user-list-create'),
|
||||
path('user/', views.UserLstView.as_view(), name='user-create-list'),
|
||||
path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -14,3 +14,8 @@ class PageInline(admin.TabularInline):
|
|||
class AdvertisementModelAdmin(admin.ModelAdmin):
|
||||
"""Admin model for model Advertisement"""
|
||||
inlines = (PageInline, )
|
||||
list_display = ('id', '__str__', 'block_level',
|
||||
'start', 'end', 'page_type')
|
||||
list_filter = ('url', 'block_level', 'start', 'end', 'page_type',
|
||||
'pages__source')
|
||||
date_hierarchy = 'created'
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ class AdvertisementQuerySet(models.QuerySet):
|
|||
"""Filter Advertisement by page type."""
|
||||
return self.filter(page_type__name=page_type)
|
||||
|
||||
def by_country(self, code: str):
|
||||
"""Filter Advertisement by country code."""
|
||||
return self.filter(sites__country__code=code)
|
||||
|
||||
def by_locale(self, locale):
|
||||
"""Filter by locale."""
|
||||
return self.filter(target_languages__locale=locale)
|
||||
|
|
@ -67,11 +71,11 @@ class Advertisement(ProjectBaseMixin):
|
|||
return super().delete(using, keep_parents)
|
||||
|
||||
@property
|
||||
def mobile_page(self):
|
||||
def mobile_pages(self):
|
||||
"""Return mobile page"""
|
||||
return self.pages.by_platform(Page.MOBILE).first()
|
||||
return self.pages.by_platform(Page.MOBILE)
|
||||
|
||||
@property
|
||||
def web_page(self):
|
||||
def web_pages(self):
|
||||
"""Return web page"""
|
||||
return self.pages.by_platform(Page.WEB).first()
|
||||
return self.pages.by_platform(Page.WEB)
|
||||
|
|
|
|||
|
|
@ -1,26 +1,14 @@
|
|||
"""Serializers for back office app advertisements"""
|
||||
from main.serializers import PageBaseSerializer
|
||||
from advertisement.serializers import AdvertisementBaseSerializer
|
||||
from main.serializers import PageExtendedSerializer
|
||||
|
||||
|
||||
class AdvertisementPageBaseSerializer(PageBaseSerializer):
|
||||
"""Base serializer for linking page w/ advertisement."""
|
||||
class AdvertisementDetailSerializer(AdvertisementBaseSerializer):
|
||||
"""Advertisement serializer for back office."""
|
||||
pages = PageExtendedSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta(PageBaseSerializer.Meta):
|
||||
class Meta(AdvertisementBaseSerializer.Meta):
|
||||
"""Meta class."""
|
||||
|
||||
PageBaseSerializer.Meta.extra_kwargs.update({
|
||||
'advertisement': {'write_only': True},
|
||||
'image_url': {'required': True},
|
||||
'width': {'required': True},
|
||||
'height': {'required': True},
|
||||
})
|
||||
|
||||
|
||||
class AdvertisementPageListCreateSerializer(AdvertisementPageBaseSerializer):
|
||||
"""Serializer for linking page w/ advertisement."""
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Overridden create method."""
|
||||
|
||||
validated_data['advertisement'] = self.context.get('view').get_object()
|
||||
return super().create(validated_data)
|
||||
fields = AdvertisementBaseSerializer.Meta.fields + [
|
||||
'pages',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,23 +2,20 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from advertisement import models
|
||||
from translation.serializers import LanguageSerializer
|
||||
from main.serializers import SiteShortSerializer, PageBaseSerializer
|
||||
from translation.models import Language
|
||||
from main.models import SiteSettings
|
||||
from main.serializers import PageTypeBaseSerializer
|
||||
from translation.models import Language
|
||||
|
||||
|
||||
class AdvertisementBaseSerializer(serializers.ModelSerializer):
|
||||
"""Base serializer for model Advertisement."""
|
||||
|
||||
languages = LanguageSerializer(many=True, read_only=True,
|
||||
source='target_languages')
|
||||
page_type_detail = PageTypeBaseSerializer(read_only=True,
|
||||
source='page_type')
|
||||
target_languages = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Language.objects.all(),
|
||||
many=True,
|
||||
write_only=True
|
||||
)
|
||||
sites = SiteShortSerializer(many=True, read_only=True)
|
||||
target_sites = serializers.PrimaryKeyRelatedField(
|
||||
queryset=SiteSettings.objects.all(),
|
||||
many=True,
|
||||
|
|
@ -33,22 +30,21 @@ class AdvertisementBaseSerializer(serializers.ModelSerializer):
|
|||
'uuid',
|
||||
'url',
|
||||
'block_level',
|
||||
'languages',
|
||||
'target_languages',
|
||||
'sites',
|
||||
'target_sites',
|
||||
'start',
|
||||
'end',
|
||||
'page_type',
|
||||
'page_type_detail',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'page_type': {'required': True, 'write_only': True}
|
||||
}
|
||||
|
||||
|
||||
class AdvertisementPageTypeCommonListSerializer(AdvertisementBaseSerializer):
|
||||
"""Serializer for AdvertisementPageTypeCommonView."""
|
||||
|
||||
page = PageBaseSerializer(source='common_page', read_only=True)
|
||||
|
||||
class AdvertisementSerializer(AdvertisementBaseSerializer):
|
||||
"""Serializer for model Advertisement."""
|
||||
class Meta(AdvertisementBaseSerializer.Meta):
|
||||
"""Meta class."""
|
||||
fields = AdvertisementBaseSerializer.Meta.fields + [
|
||||
'page',
|
||||
]
|
||||
fields = AdvertisementBaseSerializer.Meta.fields.copy()
|
||||
fields.pop(fields.index('page_type_detail'))
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
"""Serializers for mobile app advertisements"""
|
||||
from advertisement.serializers import AdvertisementBaseSerializer
|
||||
from advertisement.serializers import AdvertisementSerializer
|
||||
from main.serializers import PageBaseSerializer
|
||||
|
||||
|
||||
class AdvertisementPageTypeMobileListSerializer(AdvertisementBaseSerializer):
|
||||
class AdvertisementPageTypeMobileListSerializer(AdvertisementSerializer):
|
||||
"""Serializer for AdvertisementPageTypeMobileView."""
|
||||
|
||||
page = PageBaseSerializer(source='mobile_page', read_only=True)
|
||||
pages = PageBaseSerializer(many=True, source='mobile_pages', read_only=True)
|
||||
|
||||
class Meta(AdvertisementBaseSerializer.Meta):
|
||||
class Meta(AdvertisementSerializer.Meta):
|
||||
"""Meta class."""
|
||||
fields = AdvertisementBaseSerializer.Meta.fields + [
|
||||
'page',
|
||||
fields = AdvertisementSerializer.Meta.fields + [
|
||||
'pages',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
"""Serializers for web app advertisements"""
|
||||
from advertisement.serializers import AdvertisementBaseSerializer
|
||||
from advertisement.serializers import AdvertisementSerializer
|
||||
from main.serializers import PageBaseSerializer
|
||||
|
||||
|
||||
class AdvertisementPageTypeWebListSerializer(AdvertisementBaseSerializer):
|
||||
class AdvertisementPageTypeWebListSerializer(AdvertisementSerializer):
|
||||
"""Serializer for AdvertisementPageTypeWebView."""
|
||||
|
||||
page = PageBaseSerializer(source='web_page', read_only=True)
|
||||
pages = PageBaseSerializer(many=True, source='web_pages', read_only=True)
|
||||
|
||||
class Meta(AdvertisementBaseSerializer.Meta):
|
||||
class Meta(AdvertisementSerializer.Meta):
|
||||
"""Meta class."""
|
||||
fields = AdvertisementBaseSerializer.Meta.fields + [
|
||||
'page',
|
||||
fields = AdvertisementSerializer.Meta.fields + [
|
||||
'pages',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ app_name = 'advertisements'
|
|||
urlpatterns = [
|
||||
path('', views.AdvertisementListCreateView.as_view(), name='list-create'),
|
||||
path('<int:pk>/', views.AdvertisementRUDView.as_view(), name='rud'),
|
||||
path('<int:pk>/pages/', views.AdvertisementPageListCreateView.as_view(),
|
||||
name='page-list-create'),
|
||||
path('<int:ad_pk>/pages/<int:page_pk>/', views.AdvertisementPageRUDView.as_view(),
|
||||
name='page-rud')
|
||||
path('<int:pk>/pages/', views.AdvertisementPageCreateView.as_view(),
|
||||
name='ad-page-create'),
|
||||
path('<int:ad_pk>/pages/<int:page_pk>/', views.AdvertisementPageUDView.as_view(),
|
||||
name='ad-page-update-destroy')
|
||||
]
|
||||
|
||||
urlpatterns += common_urlpatterns
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
"""Back office views for app advertisement"""
|
||||
from rest_framework import generics
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from main.serializers import PageExtendedSerializer
|
||||
from advertisement.models import Advertisement
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from advertisement.serializers import (AdvertisementBaseSerializer,
|
||||
AdvertisementPageBaseSerializer,
|
||||
AdvertisementPageListCreateSerializer)
|
||||
AdvertisementDetailSerializer)
|
||||
|
||||
|
||||
class AdvertisementBackOfficeViewMixin(generics.GenericAPIView):
|
||||
"""Base back office advertisement view."""
|
||||
|
||||
pagination_class = None
|
||||
permission_classes = (permissions.IsAuthenticated, )
|
||||
|
||||
def get_queryset(self):
|
||||
|
|
@ -31,14 +31,14 @@ class AdvertisementRUDView(AdvertisementBackOfficeViewMixin,
|
|||
generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Retrieve|Update|Destroy advertisement page view."""
|
||||
|
||||
serializer_class = AdvertisementBaseSerializer
|
||||
serializer_class = AdvertisementDetailSerializer
|
||||
|
||||
|
||||
class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin,
|
||||
generics.ListCreateAPIView):
|
||||
"""Retrieve|Update|Destroy advertisement page view."""
|
||||
class AdvertisementPageCreateView(AdvertisementBackOfficeViewMixin,
|
||||
generics.CreateAPIView):
|
||||
"""Create advertisement page view."""
|
||||
|
||||
serializer_class = AdvertisementPageListCreateSerializer
|
||||
serializer_class = PageExtendedSerializer
|
||||
|
||||
def get_object(self):
|
||||
"""Returns the object the view is displaying."""
|
||||
|
|
@ -56,12 +56,19 @@ class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin,
|
|||
"""Overridden get_queryset method."""
|
||||
return self.get_object().pages.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Overridden create method."""
|
||||
request.data.update({'advertisement': self.get_object().pk})
|
||||
super().create(request, *args, **kwargs)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
class AdvertisementPageRUDView(AdvertisementBackOfficeViewMixin,
|
||||
generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Create|Retrieve|Update|Destroy advertisement page view."""
|
||||
|
||||
serializer_class = AdvertisementPageBaseSerializer
|
||||
class AdvertisementPageUDView(AdvertisementBackOfficeViewMixin,
|
||||
generics.UpdateAPIView,
|
||||
generics.DestroyAPIView):
|
||||
"""Update|Destroy advertisement page view."""
|
||||
|
||||
serializer_class = PageExtendedSerializer
|
||||
|
||||
def get_object(self):
|
||||
"""Returns the object the view is displaying."""
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ from rest_framework import generics
|
|||
from rest_framework import permissions
|
||||
|
||||
from advertisement.models import Advertisement
|
||||
from advertisement.serializers import AdvertisementBaseSerializer, \
|
||||
AdvertisementPageTypeCommonListSerializer
|
||||
from advertisement.serializers import AdvertisementBaseSerializer
|
||||
|
||||
|
||||
class AdvertisementBaseView(generics.GenericAPIView):
|
||||
|
|
@ -16,8 +15,7 @@ class AdvertisementBaseView(generics.GenericAPIView):
|
|||
|
||||
def get_queryset(self):
|
||||
"""Overridden get queryset method."""
|
||||
return Advertisement.objects.with_base_related() \
|
||||
.by_locale(self.request.locale)
|
||||
return Advertisement.objects.with_base_related()
|
||||
|
||||
|
||||
class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView):
|
||||
|
|
@ -28,5 +26,8 @@ class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView)
|
|||
product_type = self.kwargs.get('page_type')
|
||||
qs = super(AdvertisementPageTypeListView, self).get_queryset()
|
||||
if product_type:
|
||||
return qs.by_page_type(product_type)
|
||||
return qs.by_page_type(product_type) \
|
||||
.by_country(self.request.country_code) \
|
||||
.by_locale(self.request.locale) \
|
||||
.distinct('id')
|
||||
return qs.none()
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@ class AdvertisementPageTypeWebListView(AdvertisementPageTypeListView):
|
|||
"""Advertisement mobile list view."""
|
||||
|
||||
serializer_class = AdvertisementPageTypeWebListSerializer
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ from collection.models import Collection
|
|||
from location.models import Address
|
||||
from location.models import WineOriginAddressMixin
|
||||
from main.models import Award, Currency
|
||||
from tag.models import Tag
|
||||
from review.models import Review
|
||||
from tag.models import Tag
|
||||
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
||||
TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
|
||||
IntermediateGalleryModelMixin, HasTagsMixin,
|
||||
|
|
@ -209,23 +209,34 @@ class EstablishmentQuerySet(models.QuerySet):
|
|||
"""
|
||||
return self.annotate(mark_similarity=ExpressionWrapper(
|
||||
mark - F('intermediate_public_mark'),
|
||||
output_field=models.FloatField()
|
||||
output_field=models.FloatField(default=0)
|
||||
))
|
||||
|
||||
def similar(self, establishment_slug: str):
|
||||
def similar_base(self, establishment):
|
||||
|
||||
filters = {
|
||||
'reviews__status': Review.READY,
|
||||
'establishment_type': establishment.establishment_type,
|
||||
}
|
||||
if establishment.establishment_subtypes.exists():
|
||||
filters.update({'establishment_subtypes__in': establishment.establishment_subtypes.all()})
|
||||
return self.exclude(id=establishment.id) \
|
||||
.filter(**filters) \
|
||||
.annotate_distance(point=establishment.location)
|
||||
|
||||
def similar_restaurants(self, slug):
|
||||
"""
|
||||
Return QuerySet with objects that similar to Establishment.
|
||||
:param establishment_slug: str Establishment slug
|
||||
Return QuerySet with objects that similar to Restaurant.
|
||||
:param restaurant_slug: str Establishment slug
|
||||
"""
|
||||
establishment_qs = self.filter(slug=establishment_slug,
|
||||
public_mark__isnull=False)
|
||||
if establishment_qs.exists():
|
||||
establishment = establishment_qs.first()
|
||||
restaurant_qs = self.filter(slug=slug,
|
||||
public_mark__isnull=False)
|
||||
if restaurant_qs.exists():
|
||||
establishment = restaurant_qs.first()
|
||||
subquery_filter_by_distance = Subquery(
|
||||
self.exclude(slug=establishment_slug)
|
||||
.filter(image_url__isnull=False, public_mark__gte=10)
|
||||
.has_published_reviews()
|
||||
.annotate_distance(point=establishment.location)
|
||||
self.similar_base(establishment)
|
||||
.filter(public_mark__gte=10,
|
||||
establishment_gallery__is_main=True)
|
||||
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
|
||||
.values('id')
|
||||
)
|
||||
|
|
@ -234,6 +245,36 @@ class EstablishmentQuerySet(models.QuerySet):
|
|||
.annotate_mark_similarity(mark=establishment.public_mark) \
|
||||
.order_by('mark_similarity') \
|
||||
.distinct('mark_similarity', 'id')
|
||||
|
||||
def by_wine_region(self, wine_region):
|
||||
"""
|
||||
Return filtered QuerySet by wine region in wine origin.
|
||||
:param wine_region: wine region.
|
||||
"""
|
||||
return self.filter(wine_origin__wine_region=wine_region).distinct()
|
||||
|
||||
def by_wine_sub_region(self, wine_sub_region):
|
||||
"""
|
||||
Return filtered QuerySet by wine region in wine origin.
|
||||
:param wine_sub_region: wine sub region.
|
||||
"""
|
||||
return self.filter(wine_origin__wine_sub_region=wine_sub_region).distinct()
|
||||
|
||||
def similar_wineries(self, slug: str):
|
||||
"""
|
||||
Return QuerySet with objects that similar to Winery.
|
||||
:param establishment_slug: str Establishment slug
|
||||
"""
|
||||
winery_qs = self.filter(slug=slug)
|
||||
if winery_qs.exists():
|
||||
winery = winery_qs.first()
|
||||
return self.similar_base(winery) \
|
||||
.order_by(F('wine_origins__wine_region').asc(),
|
||||
F('wine_origins__wine_sub_region').asc()) \
|
||||
.annotate_distance(point=winery.location) \
|
||||
.order_by('distance') \
|
||||
.distinct('distance', 'wine_origins__wine_region',
|
||||
'wine_origins__wine_sub_region', 'id')
|
||||
else:
|
||||
return self.none()
|
||||
|
||||
|
|
@ -457,15 +498,9 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
|
|||
def visible_tags(self):
|
||||
return super().visible_tags \
|
||||
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
|
||||
'business_tag', 'business_tags_de']) \
|
||||
.exclude(value__in=['rss', 'rss_selection'])
|
||||
'business_tag', 'business_tags_de', 'tag'])
|
||||
# todo: recalculate toque_number
|
||||
|
||||
@property
|
||||
def visible_tags_detail(self):
|
||||
"""Removes some tags from detail Establishment representation"""
|
||||
return self.visible_tags.exclude(category__index_name__in=['tag'])
|
||||
|
||||
def recalculate_toque_number(self):
|
||||
toque_number = 0
|
||||
if self.address and self.public_mark:
|
||||
|
|
|
|||
|
|
@ -450,14 +450,18 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer):
|
|||
|
||||
address = AddressDetailSerializer(read_only=True)
|
||||
schedule = ScheduleRUDSerializer(many=True, allow_null=True)
|
||||
establishment_type = EstablishmentTypeGeoSerializer()
|
||||
artisan_category = TagBaseSerializer(many=True, allow_null=True)
|
||||
type = EstablishmentTypeGeoSerializer(source='establishment_type')
|
||||
artisan_category = TagBaseSerializer(many=True, allow_null=True, read_only=True)
|
||||
restaurant_category = TagBaseSerializer(many=True, allow_null=True, read_only=True)
|
||||
restaurant_cuisine = TagBaseSerializer(many=True, allow_null=True, read_only=True)
|
||||
|
||||
class Meta(EstablishmentBaseSerializer.Meta):
|
||||
fields = EstablishmentBaseSerializer.Meta.fields + [
|
||||
'schedule',
|
||||
'establishment_type',
|
||||
'type',
|
||||
'artisan_category',
|
||||
'restaurant_category',
|
||||
'restaurant_cuisine',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ from account.models import User
|
|||
from rest_framework import status
|
||||
from http.cookies import SimpleCookie
|
||||
from main.models import Currency
|
||||
from establishment.models import Establishment, EstablishmentType, Menu, SocialChoice, SocialNetwork
|
||||
from establishment.models import Establishment, EstablishmentType, EstablishmentSubType,\
|
||||
Menu, SocialChoice, SocialNetwork
|
||||
# Create your tests here.
|
||||
from translation.models import Language
|
||||
from account.models import Role, UserRole
|
||||
|
|
@ -87,7 +88,7 @@ class BaseTestCase(APITestCase):
|
|||
)
|
||||
|
||||
|
||||
class EstablishmentBTests(BaseTestCase):
|
||||
class EstablishmentBackTests(BaseTestCase):
|
||||
def test_establishment_CRUD(self):
|
||||
params = {'page': 1, 'page_size': 1, }
|
||||
response = self.client.get('/api/back/establishments/', params, format='json')
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ urlpatterns = [
|
|||
path('', views.EstablishmentListView.as_view(), name='list'),
|
||||
path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(),
|
||||
name='recent-reviews'),
|
||||
path('slug/<slug:slug>/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'),
|
||||
path('slug/<slug:slug>/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'),
|
||||
path('slug/<slug:slug>/comments/create/', views.EstablishmentCommentCreateView.as_view(),
|
||||
name='create-comment'),
|
||||
|
|
@ -17,4 +16,11 @@ urlpatterns = [
|
|||
name='rud-comment'),
|
||||
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
|
||||
name='create-destroy-favorites'),
|
||||
|
||||
# similar establishments
|
||||
path('slug/<slug:slug>/similar/', views.RestaurantSimilarListView.as_view(),
|
||||
name='similar-restaurants'),
|
||||
path('slug/<slug:slug>/similar/wineries/', views.WinerySimilarListView.as_view(),
|
||||
name='similar-restaurants'),
|
||||
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ from django.http import Http404, HttpResponse
|
|||
from django.shortcuts import get_object_or_404
|
||||
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
|
||||
from utils.permissions import IsCountryAdmin, IsEstablishmentManager, IsWineryReviewer
|
||||
from utils.views import CreateDestroyGalleryViewMixin
|
||||
from timetable.models import Timetable
|
||||
from rest_framework import status
|
||||
|
|
@ -25,7 +24,8 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP
|
|||
"""Establishment list/create view."""
|
||||
|
||||
filter_class = filters.EstablishmentFilter
|
||||
permission_classes = [IsCountryAdmin | IsEstablishmentManager]
|
||||
|
||||
permission_classes = [IsWineryReviewer | IsCountryAdmin | IsEstablishmentManager]
|
||||
queryset = models.Establishment.objects.all()
|
||||
serializer_class = serializers.EstablishmentListCreateSerializer
|
||||
|
||||
|
|
@ -34,14 +34,14 @@ class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView):
|
|||
lookup_field = 'slug'
|
||||
queryset = models.Establishment.objects.all()
|
||||
serializer_class = serializers.EstablishmentRUDSerializer
|
||||
permission_classes = [IsCountryAdmin | IsEstablishmentManager]
|
||||
permission_classes = [IsWineryReviewer | IsCountryAdmin | IsEstablishmentManager]
|
||||
|
||||
|
||||
class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Establishment schedule RUD view"""
|
||||
lookup_field = 'slug'
|
||||
serializer_class = ScheduleRUDSerializer
|
||||
permission_classes = [IsEstablishmentManager]
|
||||
permission_classes = [IsWineryReviewer |IsEstablishmentManager]
|
||||
|
||||
def get_object(self):
|
||||
"""
|
||||
|
|
@ -67,21 +67,21 @@ class EstablishmentScheduleCreateView(generics.CreateAPIView):
|
|||
lookup_field = 'slug'
|
||||
serializer_class = ScheduleCreateSerializer
|
||||
queryset = Timetable.objects.all()
|
||||
permission_classes = [IsEstablishmentManager]
|
||||
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
|
||||
|
||||
|
||||
class MenuListCreateView(generics.ListCreateAPIView):
|
||||
"""Menu list create view."""
|
||||
serializer_class = serializers.MenuSerializers
|
||||
queryset = models.Menu.objects.all()
|
||||
permission_classes = [IsEstablishmentManager]
|
||||
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
|
||||
|
||||
|
||||
class MenuRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Menu RUD view."""
|
||||
serializer_class = serializers.MenuRUDSerializers
|
||||
queryset = models.Menu.objects.all()
|
||||
permission_classes = [IsEstablishmentManager]
|
||||
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
|
||||
|
||||
|
||||
class SocialChoiceListCreateView(generics.ListCreateAPIView):
|
||||
|
|
@ -119,14 +119,14 @@ class PlateListCreateView(generics.ListCreateAPIView):
|
|||
serializer_class = serializers.PlatesSerializers
|
||||
queryset = models.Plate.objects.all()
|
||||
pagination_class = None
|
||||
permission_classes = [IsEstablishmentManager]
|
||||
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
|
||||
|
||||
|
||||
class PlateRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Plate RUD view."""
|
||||
serializer_class = serializers.PlatesSerializers
|
||||
queryset = models.Plate.objects.all()
|
||||
permission_classes = [IsEstablishmentManager]
|
||||
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
|
||||
|
||||
|
||||
class PhonesListCreateView(generics.ListCreateAPIView):
|
||||
|
|
@ -134,14 +134,14 @@ class PhonesListCreateView(generics.ListCreateAPIView):
|
|||
serializer_class = serializers.ContactPhoneBackSerializers
|
||||
queryset = models.ContactPhone.objects.all()
|
||||
pagination_class = None
|
||||
permission_classes = [IsEstablishmentManager]
|
||||
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
|
||||
|
||||
|
||||
class PhonesRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Phones RUD view."""
|
||||
serializer_class = serializers.ContactPhoneBackSerializers
|
||||
queryset = models.ContactPhone.objects.all()
|
||||
permission_classes = [IsEstablishmentManager]
|
||||
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
|
||||
|
||||
|
||||
class EmailListCreateView(generics.ListCreateAPIView):
|
||||
|
|
@ -149,14 +149,14 @@ class EmailListCreateView(generics.ListCreateAPIView):
|
|||
serializer_class = serializers.ContactEmailBackSerializers
|
||||
queryset = models.ContactEmail.objects.all()
|
||||
pagination_class = None
|
||||
permission_classes = [IsEstablishmentManager]
|
||||
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
|
||||
|
||||
|
||||
class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Email RUD view."""
|
||||
serializer_class = serializers.ContactEmailBackSerializers
|
||||
queryset = models.ContactEmail.objects.all()
|
||||
permission_classes = [IsEstablishmentManager]
|
||||
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
|
||||
|
||||
|
||||
class EmployeeListCreateView(generics.ListCreateAPIView):
|
||||
|
|
|
|||
|
|
@ -77,16 +77,28 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
|
|||
return qs.last_reviewed(point=point)
|
||||
|
||||
|
||||
class EstablishmentSimilarListView(EstablishmentListView):
|
||||
"""Resource for getting a list of establishments."""
|
||||
|
||||
class EstablishmentSimilarList(EstablishmentListView):
|
||||
"""Resource for getting a list of similar establishments."""
|
||||
serializer_class = serializers.EstablishmentSimilarSerializer
|
||||
pagination_class = EstablishmentPortionPagination
|
||||
|
||||
|
||||
class RestaurantSimilarListView(EstablishmentSimilarList):
|
||||
"""Resource for getting a list of similar restaurants."""
|
||||
|
||||
def get_queryset(self):
|
||||
"""Override get_queryset method"""
|
||||
qs = super().get_queryset()
|
||||
return qs.similar(establishment_slug=self.kwargs.get('slug'))
|
||||
return EstablishmentMixinView.get_queryset(self) \
|
||||
.similar_restaurants(slug=self.kwargs.get('slug'))
|
||||
|
||||
|
||||
class WinerySimilarListView(EstablishmentSimilarList):
|
||||
"""Resource for getting a list of similar wineries."""
|
||||
|
||||
def get_queryset(self):
|
||||
"""Override get_queryset method"""
|
||||
return EstablishmentMixinView.get_queryset(self) \
|
||||
.similar_wineries(slug=self.kwargs.get('slug'))
|
||||
|
||||
|
||||
class EstablishmentTypeListView(generics.ListAPIView):
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ class BaseTestCase(APITestCase):
|
|||
start=datetime.fromisoformat("2020-12-03 12:00:00"),
|
||||
end=datetime.fromisoformat("2020-12-03 12:00:00"),
|
||||
state=News.PUBLISHED,
|
||||
slug='test-news'
|
||||
slugs={'en-GB': 'test-news'}
|
||||
)
|
||||
self.slug = next(iter(self.test_news.slugs.values()))
|
||||
|
||||
self.test_content_type = ContentType.objects.get(
|
||||
app_label="news", model="news")
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ class FavoritesEstablishmentListView(generics.ListAPIView):
|
|||
"""Override get_queryset method"""
|
||||
return Establishment.objects.filter(favorites__user=self.request.user) \
|
||||
.order_by('-favorites').with_base_related() \
|
||||
.with_certain_tag_category_related('category', 'restaurant_category') \
|
||||
.with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \
|
||||
.with_certain_tag_category_related('shop_category', 'artisan_category')
|
||||
|
||||
|
||||
|
|
|
|||
24
apps/location/filters.py
Normal file
24
apps/location/filters.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from django.core.validators import EMPTY_VALUES
|
||||
from django_filters import rest_framework as filters
|
||||
|
||||
from location import models
|
||||
|
||||
|
||||
class CityBackFilter(filters.FilterSet):
|
||||
"""Employee filter set."""
|
||||
|
||||
search = filters.CharFilter(method='search_by_name')
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
||||
model = models.City
|
||||
fields = (
|
||||
'search',
|
||||
)
|
||||
|
||||
def search_by_name(self, queryset, name, value):
|
||||
"""Search by name or last name."""
|
||||
if value not in EMPTY_VALUES:
|
||||
return queryset.search_by_name(value)
|
||||
return queryset
|
||||
|
|
@ -5,6 +5,9 @@ from django.db.models.signals import post_save
|
|||
from django.db.transaction import on_commit
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from functools import reduce
|
||||
from typing import List
|
||||
|
||||
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
|
||||
|
|
@ -97,6 +100,18 @@ class Region(models.Model):
|
|||
class CityQuerySet(models.QuerySet):
|
||||
"""Extended queryset for City model."""
|
||||
|
||||
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(self, value):
|
||||
"""Search by name or last_name."""
|
||||
return self._generic_search(value, ['name', 'code', 'postal_code'])
|
||||
|
||||
def by_country_code(self, code):
|
||||
"""Return establishments by country code"""
|
||||
return self.filter(country__code=code)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly
|
|||
from django.shortcuts import get_object_or_404
|
||||
from utils.serializers import ImageBaseSerializer
|
||||
|
||||
from location import filters
|
||||
|
||||
# Address
|
||||
|
||||
|
||||
|
|
@ -31,6 +33,8 @@ class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
|
|||
"""Create view for model City."""
|
||||
serializer_class = serializers.CitySerializer
|
||||
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
|
||||
queryset = models.City.objects.all()
|
||||
filter_class = filters.CityBackFilter
|
||||
|
||||
|
||||
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||
|
|
|
|||
|
|
@ -51,3 +51,6 @@ class PageTypeAdmin(admin.ModelAdmin):
|
|||
@admin.register(models.Page)
|
||||
class PageAdmin(admin.ModelAdmin):
|
||||
"""Page admin."""
|
||||
list_display = ('id', '__str__', 'advertisement')
|
||||
list_filter = ('advertisement__url', 'source')
|
||||
date_hierarchy = 'created'
|
||||
|
|
|
|||
32
apps/main/management/commands/add_footers.py
Normal file
32
apps/main/management/commands/add_footers.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from tqdm import tqdm
|
||||
|
||||
from main.models import SiteSettings, Footer
|
||||
from transfer.models import Footers
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Add footers from legacy DB.'''
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
objects = []
|
||||
deleted = 0
|
||||
footers_list = Footers.objects.all()
|
||||
|
||||
for old_footer in tqdm(footers_list, desc='Add footers'):
|
||||
site = SiteSettings.objects.filter(old_id=old_footer.site_id).first()
|
||||
if site:
|
||||
if site.footers.exists():
|
||||
site.footers.all().delete()
|
||||
deleted += 1
|
||||
footer = Footer(
|
||||
site=site,
|
||||
about_us=old_footer.about_us,
|
||||
copyright=old_footer.copyright,
|
||||
created=old_footer.created_at,
|
||||
modified=old_footer.updated_at
|
||||
)
|
||||
objects.append(footer)
|
||||
Footer.objects.bulk_create(objects)
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'Created {len(objects)}/Deleted {deleted} footer objects.'))
|
||||
29
apps/main/migrations/0040_footer.py
Normal file
29
apps/main/migrations/0040_footer.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 2.2.7 on 2019-12-09 13:21
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0039_sitefeature_old_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Footer',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
|
||||
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('about_us', models.TextField(verbose_name='about_us')),
|
||||
('copyright', models.TextField(verbose_name='copyright')),
|
||||
('site', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='footers', to='main.SiteSettings', verbose_name='footer')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
18
apps/main/migrations/0041_auto_20191211_0631.py
Normal file
18
apps/main/migrations/0041_auto_20191211_0631.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.7 on 2019-12-11 06:31
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('advertisement', '0008_auto_20191116_1135'),
|
||||
('main', '0040_footer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='page',
|
||||
unique_together={('advertisement', 'source')},
|
||||
),
|
||||
]
|
||||
|
|
@ -305,7 +305,7 @@ class PageQuerySet(models.QuerySet):
|
|||
|
||||
def by_platform(self, platform: int):
|
||||
"""Filter by platform."""
|
||||
return self.filter(source=platform)
|
||||
return self.filter(source__in=[Page.ALL, platform])
|
||||
|
||||
|
||||
class Page(URLImageMixin, PlatformMixin, ProjectBaseMixin):
|
||||
|
|
@ -325,6 +325,7 @@ class Page(URLImageMixin, PlatformMixin, ProjectBaseMixin):
|
|||
"""Meta class."""
|
||||
verbose_name = _('page')
|
||||
verbose_name_plural = _('pages')
|
||||
unique_together = ('advertisement', 'source')
|
||||
|
||||
def __str__(self):
|
||||
"""Overridden dunder method."""
|
||||
|
|
@ -351,3 +352,12 @@ class PageType(ProjectBaseMixin):
|
|||
def __str__(self):
|
||||
"""Overridden dunder method."""
|
||||
return self.name
|
||||
|
||||
|
||||
class Footer(ProjectBaseMixin):
|
||||
site = models.ForeignKey(
|
||||
'main.SiteSettings', related_name='footers', verbose_name=_('footer'),
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
about_us = models.TextField(_('about_us'))
|
||||
copyright = models.TextField(_('copyright'))
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class FeatureSerializer(serializers.ModelSerializer):
|
|||
'site_settings',
|
||||
)
|
||||
|
||||
|
||||
class CurrencySerializer(ProjectModelSerializer):
|
||||
"""Currency serializer."""
|
||||
|
||||
|
|
@ -36,6 +37,33 @@ class CurrencySerializer(ProjectModelSerializer):
|
|||
]
|
||||
|
||||
|
||||
class FooterSerializer(serializers.ModelSerializer):
|
||||
"""Footer serializer."""
|
||||
|
||||
class Meta:
|
||||
model = models.Footer
|
||||
fields = [
|
||||
'id',
|
||||
'about_us',
|
||||
'copyright',
|
||||
'created',
|
||||
'modified',
|
||||
]
|
||||
|
||||
|
||||
class FooterBackSerializer(FooterSerializer):
|
||||
site_id = serializers.PrimaryKeyRelatedField(
|
||||
queryset=models.SiteSettings.objects.all(),
|
||||
source='site'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.Footer
|
||||
fields = FooterSerializer.Meta.fields + [
|
||||
'site_id'
|
||||
]
|
||||
|
||||
|
||||
class SiteFeatureSerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(source='feature.id')
|
||||
slug = serializers.CharField(source='feature.slug')
|
||||
|
|
@ -68,6 +96,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
|
|||
|
||||
country_name = serializers.CharField(source='country.name_translated', read_only=True)
|
||||
time_format = serializers.CharField(source='country.time_format', read_only=True)
|
||||
footers = FooterSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
|
@ -87,6 +116,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
|
|||
'published_features',
|
||||
'currency',
|
||||
'country_name',
|
||||
'footers',
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -122,8 +152,6 @@ class SiteShortSerializer(serializers.ModelSerializer):
|
|||
]
|
||||
|
||||
|
||||
|
||||
|
||||
class AwardBaseSerializer(serializers.ModelSerializer):
|
||||
"""Award base serializer."""
|
||||
|
||||
|
|
@ -204,10 +232,26 @@ class PageBaseSerializer(serializers.ModelSerializer):
|
|||
'advertisement',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'establishment': {'write_only': True}
|
||||
'advertisement': {'write_only': True},
|
||||
'image_url': {'required': True},
|
||||
'width': {'required': True},
|
||||
'height': {'required': True},
|
||||
}
|
||||
|
||||
|
||||
class PageExtendedSerializer(PageBaseSerializer):
|
||||
"""Extended serializer for model Page."""
|
||||
source_display = serializers.CharField(read_only=True,
|
||||
source='get_source_display')
|
||||
|
||||
class Meta(PageBaseSerializer.Meta):
|
||||
"""Meta class."""
|
||||
fields = PageBaseSerializer.Meta.fields + [
|
||||
'source',
|
||||
'source_display',
|
||||
]
|
||||
|
||||
|
||||
class PageTypeBaseSerializer(serializers.ModelSerializer):
|
||||
"""Serializer fro model PageType."""
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ urlpatterns = [
|
|||
name='site-feature-list-create'),
|
||||
path('site-feature/<int:id>/', views.SiteFeatureRUDBackView.as_view(),
|
||||
name='site-feature-rud'),
|
||||
path('footer/', views.FooterBackView.as_view(), name='footer-list-create'),
|
||||
path('footer/<int:pk>/', views.FooterRUDBackView.as_view(), name='footer-rud'),
|
||||
path('page-types/', views.PageTypeListCreateView.as_view(),
|
||||
name='page-types-list-create')
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from rest_framework import generics, permissions
|
|||
|
||||
from main import serializers
|
||||
from main.filters import AwardFilter
|
||||
from main.models import Award
|
||||
from main.models import Award, Footer, PageType
|
||||
from main.views import SiteSettingsView, SiteListView
|
||||
|
||||
|
||||
|
|
@ -67,3 +67,25 @@ class SiteSettingsBackOfficeView(SiteSettingsView):
|
|||
class SiteListBackOfficeView(SiteListView):
|
||||
"""Site settings View."""
|
||||
serializer_class = serializers.SiteSerializer
|
||||
|
||||
|
||||
class FooterBackView(generics.ListCreateAPIView):
|
||||
"""Footer back list/create view."""
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
serializer_class = serializers.FooterBackSerializer
|
||||
queryset = Footer.objects.all()
|
||||
|
||||
|
||||
class FooterRUDBackView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""Footer back RUD view."""
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
|
||||
serializer_class = serializers.FooterBackSerializer
|
||||
queryset = Footer.objects.all()
|
||||
|
||||
|
||||
class PageTypeListCreateView(generics.ListCreateAPIView):
|
||||
"""PageType back office view."""
|
||||
permission_classes = (permissions.IsAuthenticatedOrReadOnly, )
|
||||
pagination_class = None
|
||||
serializer_class = serializers.PageTypeBaseSerializer
|
||||
queryset = PageType.objects.all()
|
||||
|
|
|
|||
18
apps/news/migrations/0038_news_backoffice_title.py
Normal file
18
apps/news/migrations/0038_news_backoffice_title.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.7 on 2019-12-10 12:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0037_auto_20191129_1320'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='news',
|
||||
name='backoffice_title',
|
||||
field=models.TextField(default=None, null=True, verbose_name='Title for searching via BO'),
|
||||
),
|
||||
]
|
||||
29
apps/news/migrations/0039_news_slugs.py
Normal file
29
apps/news/migrations/0039_news_slugs.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 2.2.7 on 2019-12-10 13:49
|
||||
|
||||
import django.contrib.postgres.fields.hstore
|
||||
from django.db import migrations
|
||||
from django.contrib.postgres.operations import HStoreExtension
|
||||
|
||||
def migrate_slugs(apps, schemaeditor):
|
||||
News = apps.get_model('news', 'News')
|
||||
for news in News.objects.all():
|
||||
if news.slug:
|
||||
news.slugs = {'en-GB': news.slug}
|
||||
news.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0038_news_backoffice_title'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
HStoreExtension(),
|
||||
migrations.AddField(
|
||||
model_name='news',
|
||||
name='slugs',
|
||||
field=django.contrib.postgres.fields.hstore.HStoreField(blank=True, default=None, help_text='{"en-GB":"some slug"}', null=True, verbose_name='Slugs for current news obj'),
|
||||
),
|
||||
migrations.RunPython(migrate_slugs, migrations.RunPython.noop)
|
||||
]
|
||||
17
apps/news/migrations/0040_remove_news_slug.py
Normal file
17
apps/news/migrations/0040_remove_news_slug.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 2.2.7 on 2019-12-10 16:22
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('news', '0039_news_slugs'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='news',
|
||||
name='slug',
|
||||
),
|
||||
]
|
||||
|
|
@ -12,6 +12,7 @@ from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, Has
|
|||
FavoritesMixin)
|
||||
from utils.querysets import TranslationQuerysetMixin
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.fields import HStoreField
|
||||
|
||||
|
||||
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
|
||||
|
|
@ -168,6 +169,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
|||
title = TJSONField(blank=True, null=True, default=None,
|
||||
verbose_name=_('title'),
|
||||
help_text='{"en-GB":"some text"}')
|
||||
backoffice_title = models.TextField(null=True, default=None,
|
||||
verbose_name=_('Title for searching via BO'))
|
||||
subtitle = TJSONField(blank=True, null=True, default=None,
|
||||
verbose_name=_('subtitle'),
|
||||
help_text='{"en-GB":"some text"}')
|
||||
|
|
@ -178,8 +181,9 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
|||
verbose_name=_('Start'))
|
||||
end = models.DateTimeField(blank=True, null=True, default=None,
|
||||
verbose_name=_('End'))
|
||||
slug = models.SlugField(unique=True, max_length=255,
|
||||
verbose_name=_('News slug'))
|
||||
slugs = HStoreField(null=True, blank=True, default=None,
|
||||
verbose_name=_('Slugs for current news obj'),
|
||||
help_text='{"en-GB":"some slug"}')
|
||||
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
|
||||
verbose_name=_('State'))
|
||||
is_highlighted = models.BooleanField(default=False,
|
||||
|
|
@ -228,7 +232,7 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
|
|||
|
||||
@property
|
||||
def web_url(self):
|
||||
return reverse('web:news:rud', kwargs={'slug': self.slug})
|
||||
return reverse('web:news:rud', kwargs={'slug': next(iter(self.slugs.values()))})
|
||||
|
||||
def should_read(self, user):
|
||||
return self.__class__.objects.should_read(self, user)[:3]
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ class NewsBaseSerializer(ProjectModelSerializer):
|
|||
'is_highlighted',
|
||||
'news_type',
|
||||
'tags',
|
||||
'slug',
|
||||
'slugs',
|
||||
'view_counter',
|
||||
)
|
||||
|
||||
|
|
@ -169,9 +169,31 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
|
|||
|
||||
fields = NewsBaseSerializer.Meta.fields + (
|
||||
'title',
|
||||
'backoffice_title',
|
||||
'subtitle',
|
||||
'is_published',
|
||||
)
|
||||
extra_kwargs = {
|
||||
'backoffice_title': {'allow_null': False},
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
slugs = validated_data.get('slugs')
|
||||
if slugs:
|
||||
if models.News.objects.filter(
|
||||
slugs__values__contains=list(slugs.values())
|
||||
).exists():
|
||||
raise serializers.ValidationError({'slugs': _('News with this slug already exists.')})
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
slugs = validated_data.get('slugs')
|
||||
if slugs:
|
||||
if models.News.objects.filter(
|
||||
slugs__values__contains=list(slugs.values())
|
||||
).exclude(pk=instance.pk).exists():
|
||||
raise serializers.ValidationError({'slugs': _('News with this slug already exists.')})
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
|
||||
|
|
@ -252,7 +274,7 @@ class NewsFavoritesCreateSerializer(FavoritesCreateSerializer):
|
|||
def validate(self, attrs):
|
||||
"""Overridden validate method"""
|
||||
# Check establishment object
|
||||
news_qs = models.News.objects.filter(slug=self.slug)
|
||||
news_qs = models.News.objects.filter(slugs__values__contains=[self.slug])
|
||||
|
||||
# Check establishment obj by slug from lookup_kwarg
|
||||
if not news_qs.exists():
|
||||
|
|
|
|||
|
|
@ -66,10 +66,11 @@ class BaseTestCase(APITestCase):
|
|||
start=datetime.now() + timedelta(hours=-2),
|
||||
end=datetime.now() + timedelta(hours=2),
|
||||
state=News.PUBLISHED,
|
||||
slug='test-news-slug',
|
||||
slugs={'en-GB': 'test-news-slug'},
|
||||
country=self.country_ru,
|
||||
site=self.site_ru
|
||||
)
|
||||
self.slug = next(iter(self.test_news.slugs.values()))
|
||||
|
||||
|
||||
class NewsTestCase(BaseTestCase):
|
||||
|
|
@ -84,7 +85,7 @@ class NewsTestCase(BaseTestCase):
|
|||
"start": datetime.now() + timedelta(hours=-2),
|
||||
"end": datetime.now() + timedelta(hours=2),
|
||||
"state": News.PUBLISHED,
|
||||
"slug": 'test-news-slug_post',
|
||||
"slugs": {'en-GB': 'test-news-slug_post'},
|
||||
"country_id": self.country_ru.id,
|
||||
"site_id": self.site_ru.id
|
||||
}
|
||||
|
|
@ -97,7 +98,7 @@ class NewsTestCase(BaseTestCase):
|
|||
response = self.client.get(reverse('web:news:list'))
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
response = self.client.get(f"/api/web/news/slug/{self.test_news.slug}/")
|
||||
response = self.client.get(f"/api/web/news/slug/{self.slug}/")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
response = self.client.get("/api/web/news/types/")
|
||||
|
|
@ -117,7 +118,7 @@ class NewsTestCase(BaseTestCase):
|
|||
data = {
|
||||
'id': self.test_news.id,
|
||||
'description': {"ru-RU": "Description test news!"},
|
||||
'slug': self.test_news.slug,
|
||||
'slugs': self.test_news.slugs,
|
||||
'start': self.test_news.start,
|
||||
'news_type_id': self.test_news.news_type_id,
|
||||
'country_id': self.country_ru.id,
|
||||
|
|
@ -133,10 +134,10 @@ class NewsTestCase(BaseTestCase):
|
|||
"object_id": self.test_news.id
|
||||
}
|
||||
|
||||
response = self.client.post(f'/api/web/news/slug/{self.test_news.slug}/favorites/', data=data)
|
||||
response = self.client.post(f'/api/web/news/slug/{self.slug}/favorites/', data=data)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
response = self.client.delete(f'/api/web/news/slug/{self.test_news.slug}/favorites/', format='json')
|
||||
response = self.client.delete(f'/api/web/news/slug/{self.slug}/favorites/', format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ class NewsMixinView:
|
|||
qs = qs.by_country_code(country_code)
|
||||
return qs
|
||||
|
||||
def get_object(self):
|
||||
return self.get_queryset() \
|
||||
.filter(slugs__values__contains=[self.kwargs['slug']]).first()
|
||||
|
||||
|
||||
class NewsListView(NewsMixinView, generics.ListAPIView):
|
||||
"""News list view."""
|
||||
|
|
@ -46,7 +50,7 @@ class NewsListView(NewsMixinView, generics.ListAPIView):
|
|||
class NewsDetailView(NewsMixinView, generics.RetrieveAPIView):
|
||||
"""News detail view."""
|
||||
|
||||
lookup_field = 'slug'
|
||||
lookup_field = None
|
||||
serializer_class = serializers.NewsDetailWebSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from tqdm import tqdm
|
|||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Add add product tags networks from old db to new db.
|
||||
help = '''Add product tags networks from old db to new db.
|
||||
Run after add_product!!!'''
|
||||
|
||||
def category_sql(self):
|
||||
|
|
@ -101,14 +101,12 @@ 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('''
|
||||
|
|
|
|||
20
apps/product/management/commands/check_serial_number.py
Normal file
20
apps/product/management/commands/check_serial_number.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from tag.models import Tag, TagCategory
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Check product serial number from old db to new db.
|
||||
Run after add_product_tag!!!'''
|
||||
|
||||
def check_serial_number(self):
|
||||
category = TagCategory.objects.get(index_name='serial_number')
|
||||
tags = Tag.objects.filter(category=category, products__isnull=False)
|
||||
for tag in tqdm(tags, desc='Update serial number for product'):
|
||||
tag.products.all().update(serial_number=tag.value)
|
||||
tag.products.clear()
|
||||
|
||||
self.stdout.write(self.style.WARNING(f'Check serial number product end.'))
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
self.check_serial_number()
|
||||
18
apps/product/migrations/0019_product_serial_number.py
Normal file
18
apps/product/migrations/0019_product_serial_number.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.7 on 2019-11-21 09:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0018_purchasedproduct'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='serial_number',
|
||||
field=models.CharField(default=None, max_length=255, null=True, verbose_name='Serial number'),
|
||||
),
|
||||
]
|
||||
14
apps/product/migrations/0020_merge_20191209_0911.py
Normal file
14
apps/product/migrations/0020_merge_20191209_0911.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Generated by Django 2.2.7 on 2019-12-09 09:11
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0019_auto_20191204_1420'),
|
||||
('product', '0019_product_serial_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
|
|
@ -218,6 +218,10 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
|
|||
comments = generic.GenericRelation(to='comment.Comment')
|
||||
awards = generic.GenericRelation(to='main.Award', related_query_name='product')
|
||||
|
||||
serial_number = models.CharField(max_length=255,
|
||||
default=None, null=True,
|
||||
verbose_name=_('Serial number'))
|
||||
|
||||
objects = ProductManager.from_queryset(ProductQuerySet)()
|
||||
|
||||
class Meta:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ from comment.models import Comment
|
|||
from comment.serializers import CommentSerializer
|
||||
from establishment.serializers import EstablishmentProductShortSerializer
|
||||
from establishment.serializers.common import _EstablishmentAddressShortSerializer
|
||||
from location.serializers import WineOriginRegionBaseSerializer, WineOriginBaseSerializer
|
||||
from location.serializers import WineOriginRegionBaseSerializer,\
|
||||
WineOriginBaseSerializer, EstablishmentWineOriginBaseSerializer
|
||||
from main.serializers import AwardSerializer
|
||||
from product import models
|
||||
from review.serializers import ReviewShortSerializer
|
||||
|
|
@ -95,6 +96,7 @@ class ProductBaseSerializer(serializers.ModelSerializer):
|
|||
preview_image_url = serializers.URLField(allow_null=True,
|
||||
read_only=True)
|
||||
in_favorites = serializers.BooleanField(allow_null=True)
|
||||
wine_origins = EstablishmentWineOriginBaseSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
|
@ -113,6 +115,7 @@ class ProductBaseSerializer(serializers.ModelSerializer):
|
|||
'wine_regions',
|
||||
'wine_colors',
|
||||
'in_favorites',
|
||||
'wine_origins',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from establishment import models
|
|||
|
||||
EstablishmentIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__,
|
||||
'establishment'))
|
||||
EstablishmentIndex.settings(number_of_shards=1, number_of_replicas=1)
|
||||
EstablishmentIndex.settings(number_of_shards=5, number_of_replicas=2)
|
||||
|
||||
|
||||
@EstablishmentIndex.doc_type
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ class NewsDocument(Document):
|
|||
'name': fields.KeywordField()})
|
||||
title = fields.ObjectField(attr='title_indexing',
|
||||
properties=OBJECT_FIELD_PROPERTIES)
|
||||
slugs = fields.ObjectField(properties=OBJECT_FIELD_PROPERTIES)
|
||||
backoffice_title = fields.TextField(analyzer='english')
|
||||
subtitle = fields.ObjectField(attr='subtitle_indexing',
|
||||
properties=OBJECT_FIELD_PROPERTIES)
|
||||
description = fields.ObjectField(attr='description_indexing',
|
||||
|
|
@ -43,13 +45,16 @@ class NewsDocument(Document):
|
|||
multi=True)
|
||||
favorites_for_users = fields.ListField(field=fields.IntegerField())
|
||||
start = fields.DateField(attr='start')
|
||||
|
||||
def prepare_slugs(self, instance):
|
||||
return {locale: instance.slugs.get(locale) for locale in OBJECT_FIELD_PROPERTIES}
|
||||
|
||||
class Django:
|
||||
|
||||
model = models.News
|
||||
fields = (
|
||||
'id',
|
||||
'end',
|
||||
'slug',
|
||||
'state',
|
||||
'is_highlighted',
|
||||
'template',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
from django.conf import settings
|
||||
from django_elasticsearch_dsl import Document, Index, fields
|
||||
from tag import models
|
||||
from news.models import News
|
||||
|
||||
TagCategoryIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'tag_category'))
|
||||
TagCategoryIndex.settings(number_of_shards=2, number_of_replicas=2)
|
||||
|
|
@ -26,8 +27,20 @@ class TagCategoryDocument(Document):
|
|||
'public',
|
||||
'value_type'
|
||||
)
|
||||
related_models = [models.Tag]
|
||||
related_models = [models.Tag, News]
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().with_base_related()
|
||||
|
||||
def get_instances_from_related(self, related_instance):
|
||||
"""If related_models is set, define how to retrieve the Car instance(s) from the related model.
|
||||
The related_models option should be used with caution because it can lead in the index
|
||||
to the updating of a lot of items.
|
||||
"""
|
||||
if isinstance(related_instance, News):
|
||||
tag_categories = []
|
||||
for tag in related_instance.tags.all():
|
||||
if tag.category not in tag_categories:
|
||||
tag_categories.append(tag.category)
|
||||
return tag_categories
|
||||
|
|
@ -221,7 +221,7 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
|
|||
'news_type',
|
||||
'tags',
|
||||
'start',
|
||||
'slug',
|
||||
'slugs',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -243,8 +243,8 @@ class WineOriginSerializer(serializers.Serializer):
|
|||
class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
|
||||
"""Establishment document serializer."""
|
||||
|
||||
establishment_type = EstablishmentTypeSerializer()
|
||||
establishment_subtypes = EstablishmentTypeSerializer(many=True)
|
||||
type = EstablishmentTypeSerializer(source='establishment_type')
|
||||
subtypes = EstablishmentTypeSerializer(many=True, source='establishment_subtypes')
|
||||
address = AddressDocumentSerializer(allow_null=True)
|
||||
tags = TagsDocumentSerializer(many=True, source='visible_tags')
|
||||
restaurant_category = TagsDocumentSerializer(many=True, allow_null=True)
|
||||
|
|
@ -280,8 +280,8 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
|
|||
'wine_origins',
|
||||
# 'works_now',
|
||||
# 'collections',
|
||||
# 'establishment_type',
|
||||
# 'establishment_subtypes',
|
||||
'type',
|
||||
'subtypes',
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -314,7 +314,8 @@ class MobileEstablishmentDocumentViewSet(EstablishmentDocumentViewSet):
|
|||
filter_backends = [
|
||||
FilteringFilterBackend,
|
||||
filters.CustomSearchFilterBackend,
|
||||
GeoSpatialFilteringFilterBackend,
|
||||
filters.CustomGeoSpatialFilteringFilterBackend,
|
||||
GeoSpatialOrderingFilterBackend,
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@
|
|||
from rest_framework import serializers
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
|
||||
from establishment.models import (Establishment, EstablishmentType)
|
||||
from news.models import News, NewsType
|
||||
from establishment.models import Establishment
|
||||
from establishment.models import EstablishmentType
|
||||
from news.models import News
|
||||
from news.models import NewsType
|
||||
from tag import models
|
||||
from utils.exceptions import (ObjectAlreadyAdded, BindingObjectNotFound,
|
||||
RemovedBindingObjectNotFound)
|
||||
from utils.exceptions import BindingObjectNotFound
|
||||
from utils.exceptions import ObjectAlreadyAdded
|
||||
from utils.exceptions import RemovedBindingObjectNotFound
|
||||
from utils.serializers import TranslatedField
|
||||
|
||||
|
||||
|
|
@ -95,6 +98,72 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
|
|||
return TagBaseSerializer(instance=tags, many=True, read_only=True).data
|
||||
|
||||
|
||||
class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for model TagCategory."""
|
||||
|
||||
label_translated = TranslatedField()
|
||||
filters = SerializerMethodField()
|
||||
param_name = SerializerMethodField()
|
||||
type = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
||||
model = models.TagCategory
|
||||
fields = (
|
||||
'id',
|
||||
'label_translated',
|
||||
'index_name',
|
||||
'param_name',
|
||||
'type',
|
||||
'filters',
|
||||
)
|
||||
|
||||
def get_type(self, obj):
|
||||
return obj in ['open_now', ]
|
||||
|
||||
def get_param_name(self, obj):
|
||||
if obj == 'service':
|
||||
return 'tags_id__in'
|
||||
|
||||
elif obj == 'pop':
|
||||
return 'tags_id__in'
|
||||
|
||||
elif obj == 'open_now':
|
||||
return 'open_now'
|
||||
|
||||
elif obj == 'wine_region':
|
||||
return 'wine_region_id__in'
|
||||
|
||||
return '%s__in' % obj.index_name
|
||||
|
||||
def get_fields(self, *args, **kwargs):
|
||||
fields = super(FiltersTagCategoryBaseSerializer, self).get_fields()
|
||||
|
||||
if self.get_type(self):
|
||||
fields.pop('filters', None)
|
||||
else:
|
||||
fields.pop('type', None)
|
||||
|
||||
return fields
|
||||
|
||||
def get_filters(self, obj):
|
||||
query_params = dict(self.context['request'].query_params)
|
||||
|
||||
params = {}
|
||||
if 'establishment_type' in query_params:
|
||||
params = {
|
||||
'establishments__isnull': False,
|
||||
}
|
||||
elif 'product_type' in query_params:
|
||||
params = {
|
||||
'products__isnull': False,
|
||||
}
|
||||
|
||||
tags = obj.tags.filter(**params).distinct()
|
||||
return TagBaseSerializer(instance=tags, many=True, read_only=True).data
|
||||
|
||||
|
||||
class TagCategoryShortSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for model TagCategory."""
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ app_name = 'tag'
|
|||
|
||||
router = SimpleRouter()
|
||||
router.register(r'categories', views.TagCategoryViewSet)
|
||||
router.register(r'filters', views.FiltersTagCategoryViewSet)
|
||||
router.register(r'chosen_tags', views.ChosenTagsView)
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
"""Tag views."""
|
||||
from django.conf import settings
|
||||
from rest_framework import generics
|
||||
from rest_framework import mixins
|
||||
from rest_framework import permissions
|
||||
from rest_framework import viewsets, mixins, status, generics
|
||||
from rest_framework import status
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from tag import filters, models, serializers
|
||||
from location.models import WineRegion
|
||||
from tag import filters
|
||||
from tag import models
|
||||
from tag import serializers
|
||||
|
||||
|
||||
class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet):
|
||||
|
|
@ -36,7 +42,8 @@ class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet):
|
|||
serializer = self.get_serializer(queryset, many=True)
|
||||
result_list = serializer.data
|
||||
if request.query_params.get('type') and (settings.ESTABLISHMENT_CHOSEN_TAGS or settings.NEWS_CHOSEN_TAGS):
|
||||
ordered_list = settings.ESTABLISHMENT_CHOSEN_TAGS if request.query_params.get('type') == 'establishment' else settings.NEWS_CHOSEN_TAGS
|
||||
ordered_list = settings.ESTABLISHMENT_CHOSEN_TAGS if request.query_params.get(
|
||||
'type') == 'establishment' else settings.NEWS_CHOSEN_TAGS
|
||||
result_list = sorted(result_list, key=lambda x: ordered_list.index(x['index_name']))
|
||||
return Response(result_list)
|
||||
|
||||
|
|
@ -53,6 +60,104 @@ class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
|||
serializer_class = serializers.TagCategoryBaseSerializer
|
||||
|
||||
|
||||
# User`s views & viewsets
|
||||
class FiltersTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
|
||||
"""ViewSet for TagCategory model."""
|
||||
|
||||
filterset_class = filters.TagCategoryFilterSet
|
||||
pagination_class = None
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
queryset = models.TagCategory.objects.with_tags().with_base_related(). \
|
||||
distinct()
|
||||
serializer_class = serializers.FiltersTagCategoryBaseSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
|
||||
result_list = serializer.data
|
||||
query_params = request.query_params
|
||||
|
||||
params_type = query_params['type']
|
||||
|
||||
if params_type == 'restaurant' and 'toque_number__in' in query_params:
|
||||
toques = {
|
||||
"index_name": "toque_number",
|
||||
"label_translated": "Toques",
|
||||
"param_name": "toque_number__in",
|
||||
"filters": [{
|
||||
"id": toque_id,
|
||||
"index_name": "toque_%d" % toque_id,
|
||||
"label_translated": "Toque %d" % toque_id
|
||||
} for toque_id in range(6)]
|
||||
}
|
||||
result_list.append(toques)
|
||||
|
||||
if params_type == 'winery' and 'wine_region_id__in' in query_params:
|
||||
try:
|
||||
wine_region_id = int(query_params['wine_region_id__in'])
|
||||
|
||||
wine_regions = {
|
||||
"index_name": "wine_region",
|
||||
"label_translated": "Wine region",
|
||||
"param_name": "wine_region_id__in",
|
||||
"filters": [{
|
||||
"id": obj.id,
|
||||
"index_name": obj.name.lower().replace(' ', '_'),
|
||||
"label_translated": obj.name
|
||||
} for obj in WineRegion.objects.filter(id=wine_region_id)]
|
||||
}
|
||||
|
||||
result_list.append(wine_regions)
|
||||
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if params_type == 'restaurant' and 'works_noon__in' in query_params:
|
||||
week_days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
|
||||
works_noon = {
|
||||
"index_name": "works_noon",
|
||||
"label_translated": "Open noon",
|
||||
"param_name": "works_noon__in",
|
||||
"filters": [{
|
||||
"id": weekday,
|
||||
"index_name": week_days[weekday].lower(),
|
||||
"label_translated": week_days[weekday]
|
||||
} for weekday in range(7)]
|
||||
}
|
||||
result_list.append(works_noon)
|
||||
|
||||
if params_type == 'restaurant' and 'works_evening__in' in query_params:
|
||||
week_days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
|
||||
works_evening = {
|
||||
"index_name": "works_evening",
|
||||
"label_translated": "Open evening",
|
||||
"param_name": "works_evening__in",
|
||||
"filters": [{
|
||||
"id": weekday,
|
||||
"index_name": week_days[weekday].lower(),
|
||||
"label_translated": week_days[weekday]
|
||||
} for weekday in range(7)]
|
||||
}
|
||||
result_list.append(works_evening)
|
||||
|
||||
if params_type in ('restaurant', 'artisan') and 'works_now' in query_params:
|
||||
works_now = {
|
||||
"index_name": "open_now",
|
||||
"label_translated": "Open now",
|
||||
"param_name": "open_now",
|
||||
"type": True
|
||||
}
|
||||
result_list.append(works_now)
|
||||
|
||||
if 'tags_id__in' in query_params:
|
||||
# filtering by params_type and tags id
|
||||
# todo: result_list.append( filtering_data )
|
||||
pass
|
||||
|
||||
return Response(result_list)
|
||||
|
||||
|
||||
# BackOffice user`s views & viewsets
|
||||
class BindObjectMixin:
|
||||
"""Bind object mixin."""
|
||||
|
|
|
|||
|
|
@ -1208,3 +1208,17 @@ class NewsletterSubscriber(MigrateMixin):
|
|||
class Meta:
|
||||
managed = False
|
||||
db_table = 'newsletter_subscriptions'
|
||||
|
||||
|
||||
class Footers(MigrateMixin):
|
||||
using = 'legacy'
|
||||
|
||||
about_us = models.TextField(blank=True, null=True)
|
||||
copyright = models.TextField(blank=True, null=True)
|
||||
site = models.ForeignKey('Sites', models.DO_NOTHING, blank=True, null=True)
|
||||
created_at = models.DateTimeField()
|
||||
updated_at = models.DateTimeField()
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
db_table = 'footers'
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class GuideSerializer(TransferSerializerMixin):
|
|||
class GuideFilterSerializer(TransferSerializerMixin):
|
||||
id = serializers.IntegerField()
|
||||
year = serializers.CharField(allow_null=True)
|
||||
establishment_type = serializers.CharField(allow_null=True)
|
||||
type = serializers.CharField(allow_null=True, source='establishment_type')
|
||||
countries = serializers.CharField(allow_null=True)
|
||||
regions = serializers.CharField(allow_null=True)
|
||||
subregions = serializers.CharField(allow_null=True)
|
||||
|
|
@ -86,7 +86,7 @@ class GuideFilterSerializer(TransferSerializerMixin):
|
|||
fields = (
|
||||
'id',
|
||||
'year',
|
||||
'establishment_type',
|
||||
'type',
|
||||
'countries',
|
||||
'regions',
|
||||
'subregions',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
"""Custom middleware."""
|
||||
"""Custom middlewares."""
|
||||
from django.utils import translation, timezone
|
||||
from account.models import User
|
||||
|
||||
from account.models import User
|
||||
from configuration.models import TranslationSettings
|
||||
from main.methods import determine_user_city
|
||||
from translation.models import Language
|
||||
|
||||
|
||||
|
|
@ -18,7 +19,11 @@ def user_last_visit(get_response):
|
|||
def middleware(request):
|
||||
response = get_response(request)
|
||||
if request.user.is_authenticated:
|
||||
User.objects.filter(pk=request.user.pk).update(last_login=timezone.now())
|
||||
User.objects.filter(pk=request.user.pk).update(**{
|
||||
'last_login': timezone.now(),
|
||||
'locale': request.locale,
|
||||
'city': determine_user_city(request),
|
||||
})
|
||||
return response
|
||||
return middleware
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ from rest_framework_simplejwt.tokens import AccessToken
|
|||
from account.models import UserRole, Role
|
||||
from authorization.models import JWTRefreshToken
|
||||
from utils.tokens import GMRefreshToken
|
||||
|
||||
from establishment.models import EstablishmentSubType
|
||||
from location.models import Address
|
||||
|
||||
class IsAuthenticatedAndTokenIsValid(permissions.BasePermission):
|
||||
"""
|
||||
|
|
@ -56,8 +57,9 @@ class IsGuest(permissions.IsAuthenticatedOrReadOnly):
|
|||
"""
|
||||
Object-level permission to only allow owners of an object to edit it.
|
||||
"""
|
||||
|
||||
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
|
||||
def has_permission(self, request, view):
|
||||
|
||||
rules = [
|
||||
request.user.is_superuser,
|
||||
request.method in permissions.SAFE_METHODS
|
||||
|
|
@ -306,7 +308,6 @@ class IsEstablishmentManager(IsStandardUser):
|
|||
rules = [
|
||||
# special!
|
||||
super().has_permission(request, view)
|
||||
# super().has_object_permission(request, view, obj)
|
||||
]
|
||||
|
||||
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \
|
||||
|
|
@ -319,7 +320,6 @@ class IsEstablishmentManager(IsStandardUser):
|
|||
).exists(),
|
||||
# special!
|
||||
super().has_permission(request, view)
|
||||
# super().has_object_permission(request, view, obj)
|
||||
]
|
||||
|
||||
return any(rules)
|
||||
|
|
@ -368,7 +368,7 @@ class IsRestaurantReviewer(IsStandardUser):
|
|||
# and request.user.email_confirmed,
|
||||
if hasattr(request.data, 'user') and hasattr(request.data, 'object_id'):
|
||||
role = Role.objects.filter(role=Role.RESTAURANT_REVIEWER) \
|
||||
.first() # 'Comments moderator'
|
||||
.first()
|
||||
|
||||
rules = [
|
||||
UserRole.objects.filter(user=request.user, role=role,
|
||||
|
|
@ -394,3 +394,58 @@ class IsRestaurantReviewer(IsStandardUser):
|
|||
]
|
||||
|
||||
return any(rules)
|
||||
|
||||
|
||||
class IsWineryReviewer(IsStandardUser):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
rules = [
|
||||
super().has_permission(request, view)
|
||||
]
|
||||
|
||||
if 'type_id' in request.data and 'address_id' in request.data and request.user:
|
||||
countries = Address.objects.filter(id=request.data['address_id'])
|
||||
|
||||
est = EstablishmentSubType.objects.filter(establishment_type_id=request.data['type_id'])
|
||||
if est.exists():
|
||||
role = Role.objects.filter(establishment_subtype_id__in=[type.id for type in est],
|
||||
role=Role.WINERY_REVIEWER,
|
||||
country_id__in=[country.id for country in countries]) \
|
||||
.first()
|
||||
|
||||
rules.append(
|
||||
UserRole.objects.filter(user=request.user, role=role).exists()
|
||||
)
|
||||
|
||||
return any(rules)
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
rules = [
|
||||
super().has_object_permission(request, view, obj)
|
||||
]
|
||||
|
||||
if hasattr(obj, 'type_id') or hasattr(obj, 'establishment_type_id'):
|
||||
type_id: int
|
||||
if hasattr(obj, 'type_id'):
|
||||
type_id = obj.type_id
|
||||
else:
|
||||
type_id = obj.establishment_type_id
|
||||
|
||||
est = EstablishmentSubType.objects.filter(establishment_type_id=type_id)
|
||||
role = Role.objects.filter(role=Role.WINERY_REVIEWER,
|
||||
establishment_subtype_id__in=[id for type.id in est],
|
||||
country_id=obj.country_id).first()
|
||||
|
||||
object_id: int
|
||||
if hasattr(obj, 'object_id'):
|
||||
object_id = obj.object_id
|
||||
else:
|
||||
object_id = obj.establishment_id
|
||||
|
||||
rules = [
|
||||
UserRole.objects.filter(user=request.user, role=role,
|
||||
establishment_id=object_id
|
||||
).exists(),
|
||||
super().has_object_permission(request, view, obj)
|
||||
]
|
||||
return any(rules)
|
||||
|
|
@ -56,17 +56,18 @@ class TranslateFieldTests(BaseTestCase):
|
|||
start=datetime.now(pytz.utc) + timedelta(hours=-13),
|
||||
end=datetime.now(pytz.utc) + timedelta(hours=13),
|
||||
news_type=self.news_type,
|
||||
slug='test',
|
||||
slugs={'en-GB': 'test'},
|
||||
state=News.PUBLISHED,
|
||||
country=self.country_ru,
|
||||
)
|
||||
self.slug = next(iter(self.news_item.slugs.values()))
|
||||
self.news_item.save()
|
||||
|
||||
def test_model_field(self):
|
||||
self.assertTrue(hasattr(self.news_item, "title_translated"))
|
||||
|
||||
def test_read_locale(self):
|
||||
response = self.client.get(f"/api/web/news/slug/{self.news_item.slug}/", format='json')
|
||||
response = self.client.get(f"/api/web/news/slug/{self.slug}/", format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
news_data = response.json()
|
||||
self.assertIn("title_translated", news_data)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from rest_framework.response import Response
|
|||
|
||||
from gallery.tasks import delete_image
|
||||
from search_indexes.documents import es_update
|
||||
from news.models import News
|
||||
|
||||
|
||||
# JWT
|
||||
|
|
@ -124,6 +125,8 @@ class BaseCreateDestroyMixinView(generics.CreateAPIView, generics.DestroyAPIView
|
|||
lookup_field = 'slug'
|
||||
|
||||
def get_base_object(self):
|
||||
if 'slugs' in [f.name for f in self._model._meta.get_fields()]: # slugs instead of `slug`
|
||||
return get_object_or_404(self._model, slugs__values__contains=[self.kwargs['slug']])
|
||||
return get_object_or_404(self._model, slug=self.kwargs['slug'])
|
||||
|
||||
def es_update_base_object(self):
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ services:
|
|||
- "5436:5432"
|
||||
volumes:
|
||||
- gm-db:/var/lib/postgresql/data/
|
||||
- .:/code
|
||||
|
||||
|
||||
elasticsearch:
|
||||
|
|
|
|||
|
|
@ -8,11 +8,15 @@
|
|||
./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 --check_serial_number
|
||||
./manage.py transfer --wine_characteristics
|
||||
./manage.py transfer --inquiries
|
||||
./manage.py transfer --assemblage
|
||||
./manage.py transfer --purchased_plaques
|
||||
./manage.py rm_empty_images
|
||||
./manage.py add_artisan_subtype # добавляет подтипы для заведений артизанов
|
||||
|
|
@ -48,6 +48,7 @@ CONTRIB_APPS = [
|
|||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.gis',
|
||||
'django.contrib.postgres',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user