Merge branch 'develop' into feature/fix-country-region-city-transfer

This commit is contained in:
Anatoly 2019-12-11 15:01:17 +03:00
commit 1b7c83899a
66 changed files with 932 additions and 178 deletions

View File

@ -1,3 +1,4 @@
FROM mdillon/postgis:10 FROM mdillon/postgis:10
RUN localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8 RUN localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8
ENV LANG ru_RU.utf8 ENV LANG ru_RU.utf8
COPY hstore.sql /docker-entrypoint-initdb.d

View File

@ -0,0 +1 @@
create extension hstore;

View 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'),
),
]

View 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'),
),
]

View File

@ -1,5 +1,6 @@
"""Account models""" """Account models"""
from datetime import datetime from datetime import datetime
from tabnanny import verbose
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager 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 rest_framework.authtoken.models import Token
from authorization.models import Application from authorization.models import Application
from establishment.models import Establishment from establishment.models import Establishment, EstablishmentSubType
from location.models import Country from location.models import Country
from main.models import SiteSettings from main.models import SiteSettings
from utils.models import GMTokenGenerator from utils.models import GMTokenGenerator
@ -33,7 +34,7 @@ class Role(ProjectBaseMixin):
REVIEWER_MANGER = 6 REVIEWER_MANGER = 6
RESTAURANT_REVIEWER = 7 RESTAURANT_REVIEWER = 7
SALES_MAN = 8 SALES_MAN = 8
WINERY_REVIEWER = 9 WINERY_REVIEWER = 9 # Establishments subtype "winery"
SELLER = 10 SELLER = 10
ROLE_CHOICES = ( ROLE_CHOICES = (
@ -54,6 +55,9 @@ class Role(ProjectBaseMixin):
null=True, blank=True, on_delete=models.SET_NULL) null=True, blank=True, on_delete=models.SET_NULL)
site = models.ForeignKey(SiteSettings, verbose_name=_('Site settings'), site = models.ForeignKey(SiteSettings, verbose_name=_('Site settings'),
null=True, blank=True, on_delete=models.SET_NULL) 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): class UserManager(BaseUserManager):
@ -103,6 +107,10 @@ class User(AbstractUser):
email_confirmed = models.BooleanField(_('email status'), default=False) email_confirmed = models.BooleanField(_('email status'), default=False)
newsletter = models.NullBooleanField(default=True) newsletter = models.NullBooleanField(default=True)
old_id = models.IntegerField(null=True, blank=True, default=None) 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' EMAIL_FIELD = 'email'
USERNAME_FIELD = 'username' USERNAME_FIELD = 'username'

View File

@ -33,11 +33,14 @@ class BackUserSerializer(serializers.ModelSerializer):
'email_confirmed', 'email_confirmed',
'newsletter', 'newsletter',
'roles', 'roles',
'password',
'city',
'locale',
) )
extra_kwargs = { 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): def create(self, validated_data):
user = super().create(validated_data) user = super().create(validated_data)

View File

@ -8,6 +8,6 @@ app_name = 'account'
urlpatterns = [ urlpatterns = [
path('role/', views.RoleLstView.as_view(), name='role-list-create'), path('role/', views.RoleLstView.as_view(), name='role-list-create'),
path('user-role/', views.UserRoleLstView.as_view(), name='user-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'), path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),
] ]

View File

@ -14,3 +14,8 @@ class PageInline(admin.TabularInline):
class AdvertisementModelAdmin(admin.ModelAdmin): class AdvertisementModelAdmin(admin.ModelAdmin):
"""Admin model for model Advertisement""" """Admin model for model Advertisement"""
inlines = (PageInline, ) 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'

View File

@ -22,6 +22,10 @@ class AdvertisementQuerySet(models.QuerySet):
"""Filter Advertisement by page type.""" """Filter Advertisement by page type."""
return self.filter(page_type__name=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): def by_locale(self, locale):
"""Filter by locale.""" """Filter by locale."""
return self.filter(target_languages__locale=locale) return self.filter(target_languages__locale=locale)
@ -67,11 +71,11 @@ class Advertisement(ProjectBaseMixin):
return super().delete(using, keep_parents) return super().delete(using, keep_parents)
@property @property
def mobile_page(self): def mobile_pages(self):
"""Return mobile page""" """Return mobile page"""
return self.pages.by_platform(Page.MOBILE).first() return self.pages.by_platform(Page.MOBILE)
@property @property
def web_page(self): def web_pages(self):
"""Return web page""" """Return web page"""
return self.pages.by_platform(Page.WEB).first() return self.pages.by_platform(Page.WEB)

View File

@ -1,26 +1,14 @@
"""Serializers for back office app advertisements""" """Serializers for back office app advertisements"""
from main.serializers import PageBaseSerializer from advertisement.serializers import AdvertisementBaseSerializer
from main.serializers import PageExtendedSerializer
class AdvertisementPageBaseSerializer(PageBaseSerializer): class AdvertisementDetailSerializer(AdvertisementBaseSerializer):
"""Base serializer for linking page w/ advertisement.""" """Advertisement serializer for back office."""
pages = PageExtendedSerializer(many=True, read_only=True)
class Meta(PageBaseSerializer.Meta): class Meta(AdvertisementBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
fields = AdvertisementBaseSerializer.Meta.fields + [
PageBaseSerializer.Meta.extra_kwargs.update({ 'pages',
'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)

View File

@ -2,23 +2,20 @@
from rest_framework import serializers from rest_framework import serializers
from advertisement import models 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.models import SiteSettings
from main.serializers import PageTypeBaseSerializer
from translation.models import Language
class AdvertisementBaseSerializer(serializers.ModelSerializer): class AdvertisementBaseSerializer(serializers.ModelSerializer):
"""Base serializer for model Advertisement.""" """Base serializer for model Advertisement."""
page_type_detail = PageTypeBaseSerializer(read_only=True,
languages = LanguageSerializer(many=True, read_only=True, source='page_type')
source='target_languages')
target_languages = serializers.PrimaryKeyRelatedField( target_languages = serializers.PrimaryKeyRelatedField(
queryset=Language.objects.all(), queryset=Language.objects.all(),
many=True, many=True,
write_only=True write_only=True
) )
sites = SiteShortSerializer(many=True, read_only=True)
target_sites = serializers.PrimaryKeyRelatedField( target_sites = serializers.PrimaryKeyRelatedField(
queryset=SiteSettings.objects.all(), queryset=SiteSettings.objects.all(),
many=True, many=True,
@ -33,22 +30,21 @@ class AdvertisementBaseSerializer(serializers.ModelSerializer):
'uuid', 'uuid',
'url', 'url',
'block_level', 'block_level',
'languages',
'target_languages', 'target_languages',
'sites',
'target_sites', 'target_sites',
'start', 'start',
'end', 'end',
'page_type',
'page_type_detail',
] ]
extra_kwargs = {
'page_type': {'required': True, 'write_only': True}
}
class AdvertisementPageTypeCommonListSerializer(AdvertisementBaseSerializer): class AdvertisementSerializer(AdvertisementBaseSerializer):
"""Serializer for AdvertisementPageTypeCommonView.""" """Serializer for model Advertisement."""
page = PageBaseSerializer(source='common_page', read_only=True)
class Meta(AdvertisementBaseSerializer.Meta): class Meta(AdvertisementBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
fields = AdvertisementBaseSerializer.Meta.fields + [ fields = AdvertisementBaseSerializer.Meta.fields.copy()
'page', fields.pop(fields.index('page_type_detail'))
]

View File

@ -1,15 +1,15 @@
"""Serializers for mobile app advertisements""" """Serializers for mobile app advertisements"""
from advertisement.serializers import AdvertisementBaseSerializer from advertisement.serializers import AdvertisementSerializer
from main.serializers import PageBaseSerializer from main.serializers import PageBaseSerializer
class AdvertisementPageTypeMobileListSerializer(AdvertisementBaseSerializer): class AdvertisementPageTypeMobileListSerializer(AdvertisementSerializer):
"""Serializer for AdvertisementPageTypeMobileView.""" """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.""" """Meta class."""
fields = AdvertisementBaseSerializer.Meta.fields + [ fields = AdvertisementSerializer.Meta.fields + [
'page', 'pages',
] ]

View File

@ -1,15 +1,15 @@
"""Serializers for web app advertisements""" """Serializers for web app advertisements"""
from advertisement.serializers import AdvertisementBaseSerializer from advertisement.serializers import AdvertisementSerializer
from main.serializers import PageBaseSerializer from main.serializers import PageBaseSerializer
class AdvertisementPageTypeWebListSerializer(AdvertisementBaseSerializer): class AdvertisementPageTypeWebListSerializer(AdvertisementSerializer):
"""Serializer for AdvertisementPageTypeWebView.""" """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.""" """Meta class."""
fields = AdvertisementBaseSerializer.Meta.fields + [ fields = AdvertisementSerializer.Meta.fields + [
'page', 'pages',
] ]

View File

@ -9,10 +9,10 @@ app_name = 'advertisements'
urlpatterns = [ urlpatterns = [
path('', views.AdvertisementListCreateView.as_view(), name='list-create'), path('', views.AdvertisementListCreateView.as_view(), name='list-create'),
path('<int:pk>/', views.AdvertisementRUDView.as_view(), name='rud'), path('<int:pk>/', views.AdvertisementRUDView.as_view(), name='rud'),
path('<int:pk>/pages/', views.AdvertisementPageListCreateView.as_view(), path('<int:pk>/pages/', views.AdvertisementPageCreateView.as_view(),
name='page-list-create'), name='ad-page-create'),
path('<int:ad_pk>/pages/<int:page_pk>/', views.AdvertisementPageRUDView.as_view(), path('<int:ad_pk>/pages/<int:page_pk>/', views.AdvertisementPageUDView.as_view(),
name='page-rud') name='ad-page-update-destroy')
] ]
urlpatterns += common_urlpatterns urlpatterns += common_urlpatterns

View File

@ -1,19 +1,19 @@
"""Back office views for app advertisement""" """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 rest_framework import permissions
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from main.serializers import PageExtendedSerializer
from advertisement.models import Advertisement from advertisement.models import Advertisement
from rest_framework.response import Response
from rest_framework import status
from advertisement.serializers import (AdvertisementBaseSerializer, from advertisement.serializers import (AdvertisementBaseSerializer,
AdvertisementPageBaseSerializer, AdvertisementDetailSerializer)
AdvertisementPageListCreateSerializer)
class AdvertisementBackOfficeViewMixin(generics.GenericAPIView): class AdvertisementBackOfficeViewMixin(generics.GenericAPIView):
"""Base back office advertisement view.""" """Base back office advertisement view."""
pagination_class = None
permission_classes = (permissions.IsAuthenticated, ) permission_classes = (permissions.IsAuthenticated, )
def get_queryset(self): def get_queryset(self):
@ -31,14 +31,14 @@ class AdvertisementRUDView(AdvertisementBackOfficeViewMixin,
generics.RetrieveUpdateDestroyAPIView): generics.RetrieveUpdateDestroyAPIView):
"""Retrieve|Update|Destroy advertisement page view.""" """Retrieve|Update|Destroy advertisement page view."""
serializer_class = AdvertisementBaseSerializer serializer_class = AdvertisementDetailSerializer
class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin, class AdvertisementPageCreateView(AdvertisementBackOfficeViewMixin,
generics.ListCreateAPIView): generics.CreateAPIView):
"""Retrieve|Update|Destroy advertisement page view.""" """Create advertisement page view."""
serializer_class = AdvertisementPageListCreateSerializer serializer_class = PageExtendedSerializer
def get_object(self): def get_object(self):
"""Returns the object the view is displaying.""" """Returns the object the view is displaying."""
@ -56,12 +56,19 @@ class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin,
"""Overridden get_queryset method.""" """Overridden get_queryset method."""
return self.get_object().pages.all() 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): def get_object(self):
"""Returns the object the view is displaying.""" """Returns the object the view is displaying."""

View File

@ -3,8 +3,7 @@ from rest_framework import generics
from rest_framework import permissions from rest_framework import permissions
from advertisement.models import Advertisement from advertisement.models import Advertisement
from advertisement.serializers import AdvertisementBaseSerializer, \ from advertisement.serializers import AdvertisementBaseSerializer
AdvertisementPageTypeCommonListSerializer
class AdvertisementBaseView(generics.GenericAPIView): class AdvertisementBaseView(generics.GenericAPIView):
@ -16,8 +15,7 @@ class AdvertisementBaseView(generics.GenericAPIView):
def get_queryset(self): def get_queryset(self):
"""Overridden get queryset method.""" """Overridden get queryset method."""
return Advertisement.objects.with_base_related() \ return Advertisement.objects.with_base_related()
.by_locale(self.request.locale)
class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView): class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView):
@ -28,5 +26,8 @@ class AdvertisementPageTypeListView(AdvertisementBaseView, generics.ListAPIView)
product_type = self.kwargs.get('page_type') product_type = self.kwargs.get('page_type')
qs = super(AdvertisementPageTypeListView, self).get_queryset() qs = super(AdvertisementPageTypeListView, self).get_queryset()
if product_type: 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() return qs.none()

View File

@ -7,3 +7,4 @@ class AdvertisementPageTypeWebListView(AdvertisementPageTypeListView):
"""Advertisement mobile list view.""" """Advertisement mobile list view."""
serializer_class = AdvertisementPageTypeWebListSerializer serializer_class = AdvertisementPageTypeWebListSerializer

View File

@ -24,8 +24,8 @@ from collection.models import Collection
from location.models import Address from location.models import Address
from location.models import WineOriginAddressMixin from location.models import WineOriginAddressMixin
from main.models import Award, Currency from main.models import Award, Currency
from tag.models import Tag
from review.models import Review from review.models import Review
from tag.models import Tag
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
IntermediateGalleryModelMixin, HasTagsMixin, IntermediateGalleryModelMixin, HasTagsMixin,
@ -209,23 +209,34 @@ class EstablishmentQuerySet(models.QuerySet):
""" """
return self.annotate(mark_similarity=ExpressionWrapper( return self.annotate(mark_similarity=ExpressionWrapper(
mark - F('intermediate_public_mark'), 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. Return QuerySet with objects that similar to Restaurant.
:param establishment_slug: str Establishment slug :param restaurant_slug: str Establishment slug
""" """
establishment_qs = self.filter(slug=establishment_slug, restaurant_qs = self.filter(slug=slug,
public_mark__isnull=False) public_mark__isnull=False)
if establishment_qs.exists(): if restaurant_qs.exists():
establishment = establishment_qs.first() establishment = restaurant_qs.first()
subquery_filter_by_distance = Subquery( subquery_filter_by_distance = Subquery(
self.exclude(slug=establishment_slug) self.similar_base(establishment)
.filter(image_url__isnull=False, public_mark__gte=10) .filter(public_mark__gte=10,
.has_published_reviews() establishment_gallery__is_main=True)
.annotate_distance(point=establishment.location)
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS] .order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
.values('id') .values('id')
) )
@ -234,6 +245,36 @@ class EstablishmentQuerySet(models.QuerySet):
.annotate_mark_similarity(mark=establishment.public_mark) \ .annotate_mark_similarity(mark=establishment.public_mark) \
.order_by('mark_similarity') \ .order_by('mark_similarity') \
.distinct('mark_similarity', 'id') .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: else:
return self.none() return self.none()
@ -457,15 +498,9 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
def visible_tags(self): def visible_tags(self):
return super().visible_tags \ return super().visible_tags \
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item', .exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
'business_tag', 'business_tags_de']) \ 'business_tag', 'business_tags_de', 'tag'])
.exclude(value__in=['rss', 'rss_selection'])
# todo: recalculate toque_number # 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): def recalculate_toque_number(self):
toque_number = 0 toque_number = 0
if self.address and self.public_mark: if self.address and self.public_mark:

View File

@ -450,14 +450,18 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer):
address = AddressDetailSerializer(read_only=True) address = AddressDetailSerializer(read_only=True)
schedule = ScheduleRUDSerializer(many=True, allow_null=True) schedule = ScheduleRUDSerializer(many=True, allow_null=True)
establishment_type = EstablishmentTypeGeoSerializer() type = EstablishmentTypeGeoSerializer(source='establishment_type')
artisan_category = TagBaseSerializer(many=True, allow_null=True) 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): class Meta(EstablishmentBaseSerializer.Meta):
fields = EstablishmentBaseSerializer.Meta.fields + [ fields = EstablishmentBaseSerializer.Meta.fields + [
'schedule', 'schedule',
'establishment_type', 'type',
'artisan_category', 'artisan_category',
'restaurant_category',
'restaurant_cuisine',
] ]

View File

@ -4,7 +4,8 @@ from account.models import User
from rest_framework import status from rest_framework import status
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
from main.models import Currency 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. # Create your tests here.
from translation.models import Language from translation.models import Language
from account.models import Role, UserRole from account.models import Role, UserRole
@ -87,7 +88,7 @@ class BaseTestCase(APITestCase):
) )
class EstablishmentBTests(BaseTestCase): class EstablishmentBackTests(BaseTestCase):
def test_establishment_CRUD(self): def test_establishment_CRUD(self):
params = {'page': 1, 'page_size': 1, } params = {'page': 1, 'page_size': 1, }
response = self.client.get('/api/back/establishments/', params, format='json') response = self.client.get('/api/back/establishments/', params, format='json')

View File

@ -9,7 +9,6 @@ urlpatterns = [
path('', views.EstablishmentListView.as_view(), name='list'), path('', views.EstablishmentListView.as_view(), name='list'),
path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(), path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(),
name='recent-reviews'), 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/', views.EstablishmentCommentListView.as_view(), name='list-comments'),
path('slug/<slug:slug>/comments/create/', views.EstablishmentCommentCreateView.as_view(), path('slug/<slug:slug>/comments/create/', views.EstablishmentCommentCreateView.as_view(),
name='create-comment'), name='create-comment'),
@ -17,4 +16,11 @@ urlpatterns = [
name='rud-comment'), name='rud-comment'),
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
name='create-destroy-favorites'), 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'),
] ]

View File

@ -3,10 +3,9 @@ from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions, status from rest_framework import generics, permissions, status
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
from establishment import filters, models, serializers from establishment import filters, models, serializers
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer 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 utils.views import CreateDestroyGalleryViewMixin
from timetable.models import Timetable from timetable.models import Timetable
from rest_framework import status from rest_framework import status
@ -25,7 +24,8 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP
"""Establishment list/create view.""" """Establishment list/create view."""
filter_class = filters.EstablishmentFilter filter_class = filters.EstablishmentFilter
permission_classes = [IsCountryAdmin | IsEstablishmentManager]
permission_classes = [IsWineryReviewer | IsCountryAdmin | IsEstablishmentManager]
queryset = models.Establishment.objects.all() queryset = models.Establishment.objects.all()
serializer_class = serializers.EstablishmentListCreateSerializer serializer_class = serializers.EstablishmentListCreateSerializer
@ -34,14 +34,14 @@ class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView):
lookup_field = 'slug' lookup_field = 'slug'
queryset = models.Establishment.objects.all() queryset = models.Establishment.objects.all()
serializer_class = serializers.EstablishmentRUDSerializer serializer_class = serializers.EstablishmentRUDSerializer
permission_classes = [IsCountryAdmin | IsEstablishmentManager] permission_classes = [IsWineryReviewer | IsCountryAdmin | IsEstablishmentManager]
class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment schedule RUD view""" """Establishment schedule RUD view"""
lookup_field = 'slug' lookup_field = 'slug'
serializer_class = ScheduleRUDSerializer serializer_class = ScheduleRUDSerializer
permission_classes = [IsEstablishmentManager] permission_classes = [IsWineryReviewer |IsEstablishmentManager]
def get_object(self): def get_object(self):
""" """
@ -67,21 +67,21 @@ class EstablishmentScheduleCreateView(generics.CreateAPIView):
lookup_field = 'slug' lookup_field = 'slug'
serializer_class = ScheduleCreateSerializer serializer_class = ScheduleCreateSerializer
queryset = Timetable.objects.all() queryset = Timetable.objects.all()
permission_classes = [IsEstablishmentManager] permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class MenuListCreateView(generics.ListCreateAPIView): class MenuListCreateView(generics.ListCreateAPIView):
"""Menu list create view.""" """Menu list create view."""
serializer_class = serializers.MenuSerializers serializer_class = serializers.MenuSerializers
queryset = models.Menu.objects.all() queryset = models.Menu.objects.all()
permission_classes = [IsEstablishmentManager] permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class MenuRUDView(generics.RetrieveUpdateDestroyAPIView): class MenuRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Menu RUD view.""" """Menu RUD view."""
serializer_class = serializers.MenuRUDSerializers serializer_class = serializers.MenuRUDSerializers
queryset = models.Menu.objects.all() queryset = models.Menu.objects.all()
permission_classes = [IsEstablishmentManager] permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class SocialChoiceListCreateView(generics.ListCreateAPIView): class SocialChoiceListCreateView(generics.ListCreateAPIView):
@ -119,14 +119,14 @@ class PlateListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.PlatesSerializers serializer_class = serializers.PlatesSerializers
queryset = models.Plate.objects.all() queryset = models.Plate.objects.all()
pagination_class = None pagination_class = None
permission_classes = [IsEstablishmentManager] permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class PlateRUDView(generics.RetrieveUpdateDestroyAPIView): class PlateRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Plate RUD view.""" """Plate RUD view."""
serializer_class = serializers.PlatesSerializers serializer_class = serializers.PlatesSerializers
queryset = models.Plate.objects.all() queryset = models.Plate.objects.all()
permission_classes = [IsEstablishmentManager] permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class PhonesListCreateView(generics.ListCreateAPIView): class PhonesListCreateView(generics.ListCreateAPIView):
@ -134,14 +134,14 @@ class PhonesListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.ContactPhoneBackSerializers serializer_class = serializers.ContactPhoneBackSerializers
queryset = models.ContactPhone.objects.all() queryset = models.ContactPhone.objects.all()
pagination_class = None pagination_class = None
permission_classes = [IsEstablishmentManager] permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class PhonesRUDView(generics.RetrieveUpdateDestroyAPIView): class PhonesRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Phones RUD view.""" """Phones RUD view."""
serializer_class = serializers.ContactPhoneBackSerializers serializer_class = serializers.ContactPhoneBackSerializers
queryset = models.ContactPhone.objects.all() queryset = models.ContactPhone.objects.all()
permission_classes = [IsEstablishmentManager] permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class EmailListCreateView(generics.ListCreateAPIView): class EmailListCreateView(generics.ListCreateAPIView):
@ -149,14 +149,14 @@ class EmailListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.ContactEmailBackSerializers serializer_class = serializers.ContactEmailBackSerializers
queryset = models.ContactEmail.objects.all() queryset = models.ContactEmail.objects.all()
pagination_class = None pagination_class = None
permission_classes = [IsEstablishmentManager] permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class EmailRUDView(generics.RetrieveUpdateDestroyAPIView): class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Email RUD view.""" """Email RUD view."""
serializer_class = serializers.ContactEmailBackSerializers serializer_class = serializers.ContactEmailBackSerializers
queryset = models.ContactEmail.objects.all() queryset = models.ContactEmail.objects.all()
permission_classes = [IsEstablishmentManager] permission_classes = [IsWineryReviewer | IsEstablishmentManager]
class EmployeeListCreateView(generics.ListCreateAPIView): class EmployeeListCreateView(generics.ListCreateAPIView):

View File

@ -77,16 +77,28 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
return qs.last_reviewed(point=point) return qs.last_reviewed(point=point)
class EstablishmentSimilarListView(EstablishmentListView): class EstablishmentSimilarList(EstablishmentListView):
"""Resource for getting a list of establishments.""" """Resource for getting a list of similar establishments."""
serializer_class = serializers.EstablishmentSimilarSerializer serializer_class = serializers.EstablishmentSimilarSerializer
pagination_class = EstablishmentPortionPagination pagination_class = EstablishmentPortionPagination
class RestaurantSimilarListView(EstablishmentSimilarList):
"""Resource for getting a list of similar restaurants."""
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method""" """Override get_queryset method"""
qs = super().get_queryset() return EstablishmentMixinView.get_queryset(self) \
return qs.similar(establishment_slug=self.kwargs.get('slug')) .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): class EstablishmentTypeListView(generics.ListAPIView):

View File

@ -42,8 +42,9 @@ class BaseTestCase(APITestCase):
start=datetime.fromisoformat("2020-12-03 12:00:00"), start=datetime.fromisoformat("2020-12-03 12:00:00"),
end=datetime.fromisoformat("2020-12-03 12:00:00"), end=datetime.fromisoformat("2020-12-03 12:00:00"),
state=News.PUBLISHED, 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( self.test_content_type = ContentType.objects.get(
app_label="news", model="news") app_label="news", model="news")

View File

@ -30,6 +30,8 @@ class FavoritesEstablishmentListView(generics.ListAPIView):
"""Override get_queryset method""" """Override get_queryset method"""
return Establishment.objects.filter(favorites__user=self.request.user) \ return Establishment.objects.filter(favorites__user=self.request.user) \
.order_by('-favorites').with_base_related() \ .order_by('-favorites').with_base_related() \
.with_certain_tag_category_related('category', 'restaurant_category') \
.with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \
.with_certain_tag_category_related('shop_category', 'artisan_category') .with_certain_tag_category_related('shop_category', 'artisan_category')

24
apps/location/filters.py Normal file
View 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

View File

@ -5,6 +5,9 @@ from django.db.models.signals import post_save
from django.db.transaction import on_commit from django.db.transaction import on_commit
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from functools import reduce
from typing import List
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
@ -97,6 +100,18 @@ class Region(models.Model):
class CityQuerySet(models.QuerySet): class CityQuerySet(models.QuerySet):
"""Extended queryset for City model.""" """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): def by_country_code(self, code):
"""Return establishments by country code""" """Return establishments by country code"""
return self.filter(country__code=code) return self.filter(country__code=code)

View File

@ -9,6 +9,8 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from utils.serializers import ImageBaseSerializer from utils.serializers import ImageBaseSerializer
from location import filters
# Address # Address
@ -31,6 +33,8 @@ class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City.""" """Create view for model City."""
serializer_class = serializers.CitySerializer serializer_class = serializers.CitySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
queryset = models.City.objects.all()
filter_class = filters.CityBackFilter
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView): class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):

View File

@ -51,3 +51,6 @@ class PageTypeAdmin(admin.ModelAdmin):
@admin.register(models.Page) @admin.register(models.Page)
class PageAdmin(admin.ModelAdmin): class PageAdmin(admin.ModelAdmin):
"""Page admin.""" """Page admin."""
list_display = ('id', '__str__', 'advertisement')
list_filter = ('advertisement__url', 'source')
date_hierarchy = 'created'

View 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.'))

View 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,
},
),
]

View 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')},
),
]

View File

@ -305,7 +305,7 @@ class PageQuerySet(models.QuerySet):
def by_platform(self, platform: int): def by_platform(self, platform: int):
"""Filter by platform.""" """Filter by platform."""
return self.filter(source=platform) return self.filter(source__in=[Page.ALL, platform])
class Page(URLImageMixin, PlatformMixin, ProjectBaseMixin): class Page(URLImageMixin, PlatformMixin, ProjectBaseMixin):
@ -325,6 +325,7 @@ class Page(URLImageMixin, PlatformMixin, ProjectBaseMixin):
"""Meta class.""" """Meta class."""
verbose_name = _('page') verbose_name = _('page')
verbose_name_plural = _('pages') verbose_name_plural = _('pages')
unique_together = ('advertisement', 'source')
def __str__(self): def __str__(self):
"""Overridden dunder method.""" """Overridden dunder method."""
@ -351,3 +352,12 @@ class PageType(ProjectBaseMixin):
def __str__(self): def __str__(self):
"""Overridden dunder method.""" """Overridden dunder method."""
return self.name 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'))

View File

@ -22,6 +22,7 @@ class FeatureSerializer(serializers.ModelSerializer):
'site_settings', 'site_settings',
) )
class CurrencySerializer(ProjectModelSerializer): class CurrencySerializer(ProjectModelSerializer):
"""Currency serializer.""" """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): class SiteFeatureSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source='feature.id') id = serializers.IntegerField(source='feature.id')
slug = serializers.CharField(source='feature.slug') 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) country_name = serializers.CharField(source='country.name_translated', read_only=True)
time_format = serializers.CharField(source='country.time_format', read_only=True) time_format = serializers.CharField(source='country.time_format', read_only=True)
footers = FooterSerializer(many=True, read_only=True)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -87,6 +116,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
'published_features', 'published_features',
'currency', 'currency',
'country_name', 'country_name',
'footers',
] ]
@ -122,8 +152,6 @@ class SiteShortSerializer(serializers.ModelSerializer):
] ]
class AwardBaseSerializer(serializers.ModelSerializer): class AwardBaseSerializer(serializers.ModelSerializer):
"""Award base serializer.""" """Award base serializer."""
@ -204,10 +232,26 @@ class PageBaseSerializer(serializers.ModelSerializer):
'advertisement', 'advertisement',
] ]
extra_kwargs = { 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): class PageTypeBaseSerializer(serializers.ModelSerializer):
"""Serializer fro model PageType.""" """Serializer fro model PageType."""

View File

@ -18,6 +18,10 @@ urlpatterns = [
name='site-feature-list-create'), name='site-feature-list-create'),
path('site-feature/<int:id>/', views.SiteFeatureRUDBackView.as_view(), path('site-feature/<int:id>/', views.SiteFeatureRUDBackView.as_view(),
name='site-feature-rud'), 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')
] ]

View File

@ -4,7 +4,7 @@ from rest_framework import generics, permissions
from main import serializers from main import serializers
from main.filters import AwardFilter from main.filters import AwardFilter
from main.models import Award from main.models import Award, Footer, PageType
from main.views import SiteSettingsView, SiteListView from main.views import SiteSettingsView, SiteListView
@ -67,3 +67,25 @@ class SiteSettingsBackOfficeView(SiteSettingsView):
class SiteListBackOfficeView(SiteListView): class SiteListBackOfficeView(SiteListView):
"""Site settings View.""" """Site settings View."""
serializer_class = serializers.SiteSerializer 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()

View 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'),
),
]

View 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)
]

View 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',
),
]

View File

@ -12,6 +12,7 @@ from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, Has
FavoritesMixin) FavoritesMixin)
from utils.querysets import TranslationQuerysetMixin from utils.querysets import TranslationQuerysetMixin
from django.conf import settings from django.conf import settings
from django.contrib.postgres.fields import HStoreField
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin): class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
@ -168,6 +169,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
title = TJSONField(blank=True, null=True, default=None, title = TJSONField(blank=True, null=True, default=None,
verbose_name=_('title'), verbose_name=_('title'),
help_text='{"en-GB":"some text"}') 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, subtitle = TJSONField(blank=True, null=True, default=None,
verbose_name=_('subtitle'), verbose_name=_('subtitle'),
help_text='{"en-GB":"some text"}') help_text='{"en-GB":"some text"}')
@ -178,8 +181,9 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
verbose_name=_('Start')) verbose_name=_('Start'))
end = models.DateTimeField(blank=True, null=True, default=None, end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('End')) verbose_name=_('End'))
slug = models.SlugField(unique=True, max_length=255, slugs = HStoreField(null=True, blank=True, default=None,
verbose_name=_('News slug')) verbose_name=_('Slugs for current news obj'),
help_text='{"en-GB":"some slug"}')
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES, state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
verbose_name=_('State')) verbose_name=_('State'))
is_highlighted = models.BooleanField(default=False, is_highlighted = models.BooleanField(default=False,
@ -228,7 +232,7 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
@property @property
def web_url(self): 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): def should_read(self, user):
return self.__class__.objects.should_read(self, user)[:3] return self.__class__.objects.should_read(self, user)[:3]

View File

@ -80,7 +80,7 @@ class NewsBaseSerializer(ProjectModelSerializer):
'is_highlighted', 'is_highlighted',
'news_type', 'news_type',
'tags', 'tags',
'slug', 'slugs',
'view_counter', 'view_counter',
) )
@ -169,9 +169,31 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
fields = NewsBaseSerializer.Meta.fields + ( fields = NewsBaseSerializer.Meta.fields + (
'title', 'title',
'backoffice_title',
'subtitle', 'subtitle',
'is_published', '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, class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
@ -252,7 +274,7 @@ class NewsFavoritesCreateSerializer(FavoritesCreateSerializer):
def validate(self, attrs): def validate(self, attrs):
"""Overridden validate method""" """Overridden validate method"""
# Check establishment object # 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 # Check establishment obj by slug from lookup_kwarg
if not news_qs.exists(): if not news_qs.exists():

View File

@ -66,10 +66,11 @@ class BaseTestCase(APITestCase):
start=datetime.now() + timedelta(hours=-2), start=datetime.now() + timedelta(hours=-2),
end=datetime.now() + timedelta(hours=2), end=datetime.now() + timedelta(hours=2),
state=News.PUBLISHED, state=News.PUBLISHED,
slug='test-news-slug', slugs={'en-GB': 'test-news-slug'},
country=self.country_ru, country=self.country_ru,
site=self.site_ru site=self.site_ru
) )
self.slug = next(iter(self.test_news.slugs.values()))
class NewsTestCase(BaseTestCase): class NewsTestCase(BaseTestCase):
@ -84,7 +85,7 @@ class NewsTestCase(BaseTestCase):
"start": datetime.now() + timedelta(hours=-2), "start": datetime.now() + timedelta(hours=-2),
"end": datetime.now() + timedelta(hours=2), "end": datetime.now() + timedelta(hours=2),
"state": News.PUBLISHED, "state": News.PUBLISHED,
"slug": 'test-news-slug_post', "slugs": {'en-GB': 'test-news-slug_post'},
"country_id": self.country_ru.id, "country_id": self.country_ru.id,
"site_id": self.site_ru.id "site_id": self.site_ru.id
} }
@ -97,7 +98,7 @@ class NewsTestCase(BaseTestCase):
response = self.client.get(reverse('web:news:list')) response = self.client.get(reverse('web:news:list'))
self.assertEqual(response.status_code, status.HTTP_200_OK) 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) self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.get("/api/web/news/types/") response = self.client.get("/api/web/news/types/")
@ -117,7 +118,7 @@ class NewsTestCase(BaseTestCase):
data = { data = {
'id': self.test_news.id, 'id': self.test_news.id,
'description': {"ru-RU": "Description test news!"}, 'description': {"ru-RU": "Description test news!"},
'slug': self.test_news.slug, 'slugs': self.test_news.slugs,
'start': self.test_news.start, 'start': self.test_news.start,
'news_type_id': self.test_news.news_type_id, 'news_type_id': self.test_news.news_type_id,
'country_id': self.country_ru.id, 'country_id': self.country_ru.id,
@ -133,10 +134,10 @@ class NewsTestCase(BaseTestCase):
"object_id": self.test_news.id "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) 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) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

View File

@ -31,6 +31,10 @@ class NewsMixinView:
qs = qs.by_country_code(country_code) qs = qs.by_country_code(country_code)
return qs return qs
def get_object(self):
return self.get_queryset() \
.filter(slugs__values__contains=[self.kwargs['slug']]).first()
class NewsListView(NewsMixinView, generics.ListAPIView): class NewsListView(NewsMixinView, generics.ListAPIView):
"""News list view.""" """News list view."""
@ -46,7 +50,7 @@ class NewsListView(NewsMixinView, generics.ListAPIView):
class NewsDetailView(NewsMixinView, generics.RetrieveAPIView): class NewsDetailView(NewsMixinView, generics.RetrieveAPIView):
"""News detail view.""" """News detail view."""
lookup_field = 'slug' lookup_field = None
serializer_class = serializers.NewsDetailWebSerializer serializer_class = serializers.NewsDetailWebSerializer
def get_queryset(self): def get_queryset(self):

View File

@ -7,7 +7,7 @@ from tqdm import tqdm
class Command(BaseCommand): 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!!!''' Run after add_product!!!'''
def category_sql(self): def category_sql(self):
@ -101,14 +101,12 @@ class Command(BaseCommand):
p.tags.clear() p.tags.clear()
print('End clear tags product') print('End clear tags product')
def remove_tags(self): def remove_tags(self):
print('Begin delete many tags') print('Begin delete many tags')
Tag.objects.\ Tag.objects.\
filter(news__isnull=True, establishments__isnull=True).delete() filter(news__isnull=True, establishments__isnull=True).delete()
print('End delete many tags') print('End delete many tags')
def product_sql(self): def product_sql(self):
with connections['legacy'].cursor() as cursor: with connections['legacy'].cursor() as cursor:
cursor.execute(''' cursor.execute('''

View 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()

View 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'),
),
]

View 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 = [
]

View File

@ -218,6 +218,10 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
comments = generic.GenericRelation(to='comment.Comment') comments = generic.GenericRelation(to='comment.Comment')
awards = generic.GenericRelation(to='main.Award', related_query_name='product') 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)() objects = ProductManager.from_queryset(ProductQuerySet)()
class Meta: class Meta:

View File

@ -6,7 +6,8 @@ from comment.models import Comment
from comment.serializers import CommentSerializer from comment.serializers import CommentSerializer
from establishment.serializers import EstablishmentProductShortSerializer from establishment.serializers import EstablishmentProductShortSerializer
from establishment.serializers.common import _EstablishmentAddressShortSerializer 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 main.serializers import AwardSerializer
from product import models from product import models
from review.serializers import ReviewShortSerializer from review.serializers import ReviewShortSerializer
@ -95,6 +96,7 @@ class ProductBaseSerializer(serializers.ModelSerializer):
preview_image_url = serializers.URLField(allow_null=True, preview_image_url = serializers.URLField(allow_null=True,
read_only=True) read_only=True)
in_favorites = serializers.BooleanField(allow_null=True) in_favorites = serializers.BooleanField(allow_null=True)
wine_origins = EstablishmentWineOriginBaseSerializer(many=True, read_only=True)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -113,6 +115,7 @@ class ProductBaseSerializer(serializers.ModelSerializer):
'wine_regions', 'wine_regions',
'wine_colors', 'wine_colors',
'in_favorites', 'in_favorites',
'wine_origins',
] ]

View File

@ -7,7 +7,7 @@ from establishment import models
EstablishmentIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, EstablishmentIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__,
'establishment')) 'establishment'))
EstablishmentIndex.settings(number_of_shards=1, number_of_replicas=1) EstablishmentIndex.settings(number_of_shards=5, number_of_replicas=2)
@EstablishmentIndex.doc_type @EstablishmentIndex.doc_type

View File

@ -17,6 +17,8 @@ class NewsDocument(Document):
'name': fields.KeywordField()}) 'name': fields.KeywordField()})
title = fields.ObjectField(attr='title_indexing', title = fields.ObjectField(attr='title_indexing',
properties=OBJECT_FIELD_PROPERTIES) properties=OBJECT_FIELD_PROPERTIES)
slugs = fields.ObjectField(properties=OBJECT_FIELD_PROPERTIES)
backoffice_title = fields.TextField(analyzer='english')
subtitle = fields.ObjectField(attr='subtitle_indexing', subtitle = fields.ObjectField(attr='subtitle_indexing',
properties=OBJECT_FIELD_PROPERTIES) properties=OBJECT_FIELD_PROPERTIES)
description = fields.ObjectField(attr='description_indexing', description = fields.ObjectField(attr='description_indexing',
@ -43,13 +45,16 @@ class NewsDocument(Document):
multi=True) multi=True)
favorites_for_users = fields.ListField(field=fields.IntegerField()) favorites_for_users = fields.ListField(field=fields.IntegerField())
start = fields.DateField(attr='start') start = fields.DateField(attr='start')
def prepare_slugs(self, instance):
return {locale: instance.slugs.get(locale) for locale in OBJECT_FIELD_PROPERTIES}
class Django: class Django:
model = models.News model = models.News
fields = ( fields = (
'id', 'id',
'end', 'end',
'slug',
'state', 'state',
'is_highlighted', 'is_highlighted',
'template', 'template',

View File

@ -2,6 +2,7 @@
from django.conf import settings from django.conf import settings
from django_elasticsearch_dsl import Document, Index, fields from django_elasticsearch_dsl import Document, Index, fields
from tag import models from tag import models
from news.models import News
TagCategoryIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'tag_category')) TagCategoryIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'tag_category'))
TagCategoryIndex.settings(number_of_shards=2, number_of_replicas=2) TagCategoryIndex.settings(number_of_shards=2, number_of_replicas=2)
@ -26,8 +27,20 @@ class TagCategoryDocument(Document):
'public', 'public',
'value_type' 'value_type'
) )
related_models = [models.Tag] related_models = [models.Tag, News]
def get_queryset(self): def get_queryset(self):
return super().get_queryset().with_base_related() 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

View File

@ -221,7 +221,7 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
'news_type', 'news_type',
'tags', 'tags',
'start', 'start',
'slug', 'slugs',
) )
@staticmethod @staticmethod
@ -243,8 +243,8 @@ class WineOriginSerializer(serializers.Serializer):
class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer): class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
"""Establishment document serializer.""" """Establishment document serializer."""
establishment_type = EstablishmentTypeSerializer() type = EstablishmentTypeSerializer(source='establishment_type')
establishment_subtypes = EstablishmentTypeSerializer(many=True) subtypes = EstablishmentTypeSerializer(many=True, source='establishment_subtypes')
address = AddressDocumentSerializer(allow_null=True) address = AddressDocumentSerializer(allow_null=True)
tags = TagsDocumentSerializer(many=True, source='visible_tags') tags = TagsDocumentSerializer(many=True, source='visible_tags')
restaurant_category = TagsDocumentSerializer(many=True, allow_null=True) restaurant_category = TagsDocumentSerializer(many=True, allow_null=True)
@ -280,8 +280,8 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
'wine_origins', 'wine_origins',
# 'works_now', # 'works_now',
# 'collections', # 'collections',
# 'establishment_type', 'type',
# 'establishment_subtypes', 'subtypes',
) )

View File

@ -314,7 +314,8 @@ class MobileEstablishmentDocumentViewSet(EstablishmentDocumentViewSet):
filter_backends = [ filter_backends = [
FilteringFilterBackend, FilteringFilterBackend,
filters.CustomSearchFilterBackend, filters.CustomSearchFilterBackend,
GeoSpatialFilteringFilterBackend, filters.CustomGeoSpatialFilteringFilterBackend,
GeoSpatialOrderingFilterBackend,
] ]

View File

@ -2,11 +2,14 @@
from rest_framework import serializers from rest_framework import serializers
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from establishment.models import (Establishment, EstablishmentType) from establishment.models import Establishment
from news.models import News, NewsType from establishment.models import EstablishmentType
from news.models import News
from news.models import NewsType
from tag import models from tag import models
from utils.exceptions import (ObjectAlreadyAdded, BindingObjectNotFound, from utils.exceptions import BindingObjectNotFound
RemovedBindingObjectNotFound) from utils.exceptions import ObjectAlreadyAdded
from utils.exceptions import RemovedBindingObjectNotFound
from utils.serializers import TranslatedField from utils.serializers import TranslatedField
@ -95,6 +98,72 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
return TagBaseSerializer(instance=tags, many=True, read_only=True).data 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): class TagCategoryShortSerializer(serializers.ModelSerializer):
"""Serializer for model TagCategory.""" """Serializer for model TagCategory."""

View File

@ -7,6 +7,7 @@ app_name = 'tag'
router = SimpleRouter() router = SimpleRouter()
router.register(r'categories', views.TagCategoryViewSet) router.register(r'categories', views.TagCategoryViewSet)
router.register(r'filters', views.FiltersTagCategoryViewSet)
router.register(r'chosen_tags', views.ChosenTagsView) router.register(r'chosen_tags', views.ChosenTagsView)
urlpatterns = [ urlpatterns = [

View File

@ -1,11 +1,17 @@
"""Tag views.""" """Tag views."""
from django.conf import settings from django.conf import settings
from rest_framework import generics
from rest_framework import mixins
from rest_framework import permissions 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.decorators import action
from rest_framework.response import Response 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): class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet):
@ -36,7 +42,8 @@ class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet):
serializer = self.get_serializer(queryset, many=True) serializer = self.get_serializer(queryset, many=True)
result_list = serializer.data result_list = serializer.data
if request.query_params.get('type') and (settings.ESTABLISHMENT_CHOSEN_TAGS or settings.NEWS_CHOSEN_TAGS): 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'])) result_list = sorted(result_list, key=lambda x: ordered_list.index(x['index_name']))
return Response(result_list) return Response(result_list)
@ -53,6 +60,104 @@ class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
serializer_class = serializers.TagCategoryBaseSerializer 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 # BackOffice user`s views & viewsets
class BindObjectMixin: class BindObjectMixin:
"""Bind object mixin.""" """Bind object mixin."""

View File

@ -1208,3 +1208,17 @@ class NewsletterSubscriber(MigrateMixin):
class Meta: class Meta:
managed = False managed = False
db_table = 'newsletter_subscriptions' 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'

View File

@ -68,7 +68,7 @@ class GuideSerializer(TransferSerializerMixin):
class GuideFilterSerializer(TransferSerializerMixin): class GuideFilterSerializer(TransferSerializerMixin):
id = serializers.IntegerField() id = serializers.IntegerField()
year = serializers.CharField(allow_null=True) 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) countries = serializers.CharField(allow_null=True)
regions = serializers.CharField(allow_null=True) regions = serializers.CharField(allow_null=True)
subregions = serializers.CharField(allow_null=True) subregions = serializers.CharField(allow_null=True)
@ -86,7 +86,7 @@ class GuideFilterSerializer(TransferSerializerMixin):
fields = ( fields = (
'id', 'id',
'year', 'year',
'establishment_type', 'type',
'countries', 'countries',
'regions', 'regions',
'subregions', 'subregions',

View File

@ -1,8 +1,9 @@
"""Custom middleware.""" """Custom middlewares."""
from django.utils import translation, timezone from django.utils import translation, timezone
from account.models import User
from account.models import User
from configuration.models import TranslationSettings from configuration.models import TranslationSettings
from main.methods import determine_user_city
from translation.models import Language from translation.models import Language
@ -18,7 +19,11 @@ def user_last_visit(get_response):
def middleware(request): def middleware(request):
response = get_response(request) response = get_response(request)
if request.user.is_authenticated: 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 response
return middleware return middleware

View File

@ -7,7 +7,8 @@ from rest_framework_simplejwt.tokens import AccessToken
from account.models import UserRole, Role from account.models import UserRole, Role
from authorization.models import JWTRefreshToken from authorization.models import JWTRefreshToken
from utils.tokens import GMRefreshToken from utils.tokens import GMRefreshToken
from establishment.models import EstablishmentSubType
from location.models import Address
class IsAuthenticatedAndTokenIsValid(permissions.BasePermission): 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. Object-level permission to only allow owners of an object to edit it.
""" """
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
def has_permission(self, request, view): def has_permission(self, request, view):
rules = [ rules = [
request.user.is_superuser, request.user.is_superuser,
request.method in permissions.SAFE_METHODS request.method in permissions.SAFE_METHODS
@ -306,7 +308,6 @@ class IsEstablishmentManager(IsStandardUser):
rules = [ rules = [
# special! # special!
super().has_permission(request, view) super().has_permission(request, view)
# super().has_object_permission(request, view, obj)
] ]
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \ role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \
@ -319,7 +320,6 @@ class IsEstablishmentManager(IsStandardUser):
).exists(), ).exists(),
# special! # special!
super().has_permission(request, view) super().has_permission(request, view)
# super().has_object_permission(request, view, obj)
] ]
return any(rules) return any(rules)
@ -368,7 +368,7 @@ class IsRestaurantReviewer(IsStandardUser):
# and request.user.email_confirmed, # and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'object_id'): if hasattr(request.data, 'user') and hasattr(request.data, 'object_id'):
role = Role.objects.filter(role=Role.RESTAURANT_REVIEWER) \ role = Role.objects.filter(role=Role.RESTAURANT_REVIEWER) \
.first() # 'Comments moderator' .first()
rules = [ rules = [
UserRole.objects.filter(user=request.user, role=role, UserRole.objects.filter(user=request.user, role=role,
@ -394,3 +394,58 @@ class IsRestaurantReviewer(IsStandardUser):
] ]
return any(rules) 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)

View File

@ -56,17 +56,18 @@ class TranslateFieldTests(BaseTestCase):
start=datetime.now(pytz.utc) + timedelta(hours=-13), start=datetime.now(pytz.utc) + timedelta(hours=-13),
end=datetime.now(pytz.utc) + timedelta(hours=13), end=datetime.now(pytz.utc) + timedelta(hours=13),
news_type=self.news_type, news_type=self.news_type,
slug='test', slugs={'en-GB': 'test'},
state=News.PUBLISHED, state=News.PUBLISHED,
country=self.country_ru, country=self.country_ru,
) )
self.slug = next(iter(self.news_item.slugs.values()))
self.news_item.save() self.news_item.save()
def test_model_field(self): def test_model_field(self):
self.assertTrue(hasattr(self.news_item, "title_translated")) self.assertTrue(hasattr(self.news_item, "title_translated"))
def test_read_locale(self): 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) self.assertEqual(response.status_code, status.HTTP_200_OK)
news_data = response.json() news_data = response.json()
self.assertIn("title_translated", news_data) self.assertIn("title_translated", news_data)

View File

@ -9,6 +9,7 @@ from rest_framework.response import Response
from gallery.tasks import delete_image from gallery.tasks import delete_image
from search_indexes.documents import es_update from search_indexes.documents import es_update
from news.models import News
# JWT # JWT
@ -124,6 +125,8 @@ class BaseCreateDestroyMixinView(generics.CreateAPIView, generics.DestroyAPIView
lookup_field = 'slug' lookup_field = 'slug'
def get_base_object(self): 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']) return get_object_or_404(self._model, slug=self.kwargs['slug'])
def es_update_base_object(self): def es_update_base_object(self):

View File

@ -32,6 +32,7 @@ services:
- "5436:5432" - "5436:5432"
volumes: volumes:
- gm-db:/var/lib/postgresql/data/ - gm-db:/var/lib/postgresql/data/
- .:/code
elasticsearch: elasticsearch:

View File

@ -8,11 +8,15 @@
./manage.py transfer --fill_city_gallery ./manage.py transfer --fill_city_gallery
./manage.py transfer -l ./manage.py transfer -l
./manage.py transfer --product ./manage.py transfer --product
# Утеряна четкая связь между последовательностью миграций для импорта тегов продуктов,
# что может привести к удалению уже импортированных тегов командой выше.
./manage.py transfer --souvenir ./manage.py transfer --souvenir
./manage.py transfer --establishment_note ./manage.py transfer --establishment_note
./manage.py transfer --product_note ./manage.py transfer --product_note
./manage.py transfer --check_serial_number
./manage.py transfer --wine_characteristics ./manage.py transfer --wine_characteristics
./manage.py transfer --inquiries ./manage.py transfer --inquiries
./manage.py transfer --assemblage ./manage.py transfer --assemblage
./manage.py transfer --purchased_plaques ./manage.py transfer --purchased_plaques
./manage.py rm_empty_images ./manage.py rm_empty_images
./manage.py add_artisan_subtype # добавляет подтипы для заведений артизанов

View File

@ -48,6 +48,7 @@ CONTRIB_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.gis', 'django.contrib.gis',
'django.contrib.postgres',
] ]